Merge feature/incremental-testing: voice API, themes, and enhanced toolbar
Incrementally tested additions that introduce no new test failures.
This commit is contained in:
commit
eaf8349203
197
src/collaboration/voiceAPI.ts
Normal file
197
src/collaboration/voiceAPI.ts
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
/**
|
||||||
|
* Voice-Enabled AI-Human Collaboration API - Ultra-optimized for injection
|
||||||
|
* Minimal footprint, maximum performance, beautiful code that gets injected everywhere
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function generateVoiceCollaborationAPI(): string {
|
||||||
|
return `
|
||||||
|
(function(){
|
||||||
|
'use strict';
|
||||||
|
try{
|
||||||
|
const w=window,d=document,c=console,n=navigator;
|
||||||
|
const SR=w.SpeechRecognition||w.webkitSpeechRecognition;
|
||||||
|
const ss=w.speechSynthesis;
|
||||||
|
let vs,cr,speaking=0,listening=0;
|
||||||
|
|
||||||
|
// Namespace protection - prevent conflicts
|
||||||
|
if(w.mcpVoiceLoaded)return;
|
||||||
|
w.mcpVoiceLoaded=1;
|
||||||
|
|
||||||
|
// Initialize voice capabilities with comprehensive error handling
|
||||||
|
const init=async()=>{
|
||||||
|
if(vs)return vs;
|
||||||
|
try{
|
||||||
|
const canSpeak=!!(ss&&ss.speak);
|
||||||
|
const canListen=!!(SR&&n.mediaDevices);
|
||||||
|
let micOK=0;
|
||||||
|
|
||||||
|
if(canListen){
|
||||||
|
try{
|
||||||
|
const s=await Promise.race([
|
||||||
|
n.mediaDevices.getUserMedia({audio:1}),
|
||||||
|
new Promise((_,reject)=>setTimeout(()=>reject('timeout'),3000))
|
||||||
|
]);
|
||||||
|
s.getTracks().forEach(t=>t.stop());
|
||||||
|
micOK=1;
|
||||||
|
}catch(e){}
|
||||||
|
}
|
||||||
|
|
||||||
|
vs={canSpeak,canListen:canListen&&micOK};
|
||||||
|
if(canSpeak&&ss.getVoices().length>0)speak('Voice collaboration active');
|
||||||
|
return vs;
|
||||||
|
}catch(e){
|
||||||
|
c.warn('[MCP] Voice init failed:',e);
|
||||||
|
vs={canSpeak:0,canListen:0};
|
||||||
|
return vs;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Ultra-compact speech synthesis with error protection
|
||||||
|
const speak=(text,opts={})=>{
|
||||||
|
try{
|
||||||
|
if(!vs?.canSpeak||speaking||!text||typeof text!=='string')return 0;
|
||||||
|
const u=new SpeechSynthesisUtterance(text.slice(0,300)); // Prevent long text issues
|
||||||
|
Object.assign(u,{rate:1,pitch:1,volume:1,...opts});
|
||||||
|
const voices=ss.getVoices();
|
||||||
|
u.voice=voices.find(v=>v.name.includes('Google')||v.name.includes('Microsoft'))||voices[0];
|
||||||
|
u.onstart=()=>speaking=1;
|
||||||
|
u.onend=u.onerror=()=>speaking=0;
|
||||||
|
ss.speak(u);
|
||||||
|
return 1;
|
||||||
|
}catch(e){c.warn('[MCP] Speak failed:',e);return 0}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Ultra-compact speech recognition with robust error handling
|
||||||
|
const listen=(timeout=10000)=>new Promise((resolve,reject)=>{
|
||||||
|
try{
|
||||||
|
if(!vs?.canListen||listening)return reject('Voice unavailable');
|
||||||
|
timeout=Math.min(Math.max(timeout||5000,1000),30000); // Clamp timeout
|
||||||
|
const r=new SR();
|
||||||
|
Object.assign(r,{continuous:0,interimResults:0,lang:'en-US'});
|
||||||
|
|
||||||
|
let resolved=0;
|
||||||
|
const cleanup=()=>{listening=0;cr=null};
|
||||||
|
|
||||||
|
r.onstart=()=>{listening=1;cr=r};
|
||||||
|
r.onresult=e=>{
|
||||||
|
if(resolved++)return;
|
||||||
|
cleanup();
|
||||||
|
const transcript=(e.results?.[0]?.[0]?.transcript||'').trim();
|
||||||
|
resolve(transcript||'');
|
||||||
|
};
|
||||||
|
r.onerror=r.onend=()=>{
|
||||||
|
if(resolved++)return;
|
||||||
|
cleanup();
|
||||||
|
reject('Recognition failed');
|
||||||
|
};
|
||||||
|
|
||||||
|
r.start();
|
||||||
|
setTimeout(()=>{if(listening&&!resolved++){r.stop();cleanup();reject('Timeout')}},timeout);
|
||||||
|
}catch(e){
|
||||||
|
listening=0;cr=null;
|
||||||
|
reject('Listen error: '+e.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Enhanced API with comprehensive safety
|
||||||
|
w.mcpNotify={
|
||||||
|
info:(msg,opts={})=>{try{c.log(\`[MCP] \${msg||''}\`);if(opts?.speak!==0)speak(msg,opts?.voice)}catch(e){}},
|
||||||
|
success:(msg,opts={})=>{try{c.log(\`[MCP] \${msg||''}\`);if(opts?.speak!==0)speak(\`Success! \${msg}\`,{...opts?.voice,pitch:1.2})}catch(e){}},
|
||||||
|
warning:(msg,opts={})=>{try{c.warn(\`[MCP] \${msg||''}\`);if(opts?.speak!==0)speak(\`Warning: \${msg}\`,{...opts?.voice,pitch:0.8})}catch(e){}},
|
||||||
|
error:(msg,opts={})=>{try{c.error(\`[MCP] \${msg||''}\`);if(opts?.speak!==0)speak(\`Error: \${msg}\`,{...opts?.voice,pitch:0.7})}catch(e){}},
|
||||||
|
speak:(text,opts={})=>speak(text,opts)
|
||||||
|
};
|
||||||
|
|
||||||
|
w.mcpPrompt=async(question,opts={})=>{
|
||||||
|
try{
|
||||||
|
if(!question||typeof question!=='string')return '';
|
||||||
|
question=question.slice(0,200); // Prevent long prompts
|
||||||
|
opts=opts||{};
|
||||||
|
|
||||||
|
if(vs?.canSpeak&&opts.speak!==0)speak(question,opts.voice);
|
||||||
|
if(opts.useVoice!==0&&vs?.canListen){
|
||||||
|
try{
|
||||||
|
const result=await listen(opts.timeout||10000);
|
||||||
|
if(vs.canSpeak)speak(\`I heard: \${result}\`,{rate:1.1});
|
||||||
|
return result;
|
||||||
|
}catch(e){
|
||||||
|
if(opts.fallback!==0&&w.prompt)return w.prompt(question);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return w.prompt?w.prompt(question):'';
|
||||||
|
}catch(e){c.warn('[MCP] Prompt failed:',e);return ''}
|
||||||
|
};
|
||||||
|
|
||||||
|
w.mcpInspector={
|
||||||
|
active:0,
|
||||||
|
start(instruction,callback,opts={}){
|
||||||
|
try{
|
||||||
|
if(this.active||!instruction||typeof instruction!=='string')return;
|
||||||
|
instruction=instruction.slice(0,100); // Prevent long instructions
|
||||||
|
this.active=1;
|
||||||
|
|
||||||
|
if(vs?.canSpeak)speak(\`\${instruction}. Click target element.\`,opts?.voice);
|
||||||
|
|
||||||
|
const indicator=d.createElement('div');
|
||||||
|
indicator.id='mcp-indicator';
|
||||||
|
indicator.innerHTML=\`<div style="position:fixed;top:20px;left:50%;transform:translateX(-50%);background:rgba(0,123,255,0.9);color:white;padding:12px 20px;border-radius:25px;font:14px -apple-system,sans-serif;z-index:999999;backdrop-filter:blur(10px);pointer-events:none;user-select:none">🎯 \${instruction}</div>\`;
|
||||||
|
|
||||||
|
// Safe DOM append with timing handling
|
||||||
|
const tryAppend=()=>{
|
||||||
|
if(d.body){
|
||||||
|
d.body.appendChild(indicator);
|
||||||
|
return 1;
|
||||||
|
}else if(d.documentElement){
|
||||||
|
d.documentElement.appendChild(indicator);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
if(!tryAppend()){
|
||||||
|
if(d.readyState==='loading'){
|
||||||
|
d.addEventListener('DOMContentLoaded',()=>tryAppend());
|
||||||
|
}else{
|
||||||
|
setTimeout(()=>tryAppend(),10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onClick=e=>{
|
||||||
|
try{
|
||||||
|
e.preventDefault();e.stopPropagation();
|
||||||
|
this.active=0;
|
||||||
|
d.removeEventListener('click',onClick,1);
|
||||||
|
indicator.remove();
|
||||||
|
if(vs?.canSpeak)speak('Got it!');
|
||||||
|
if(callback&&typeof callback==='function')callback(e.target);
|
||||||
|
}catch(err){c.warn('[MCP] Inspector click failed:',err)}
|
||||||
|
};
|
||||||
|
|
||||||
|
d.addEventListener('click',onClick,1);
|
||||||
|
setTimeout(()=>{if(this.active)this.stop()},Math.min(opts?.timeout||30000,60000));
|
||||||
|
}catch(e){c.warn('[MCP] Inspector failed:',e);this.active=0}
|
||||||
|
},
|
||||||
|
stop(){
|
||||||
|
try{
|
||||||
|
this.active=0;
|
||||||
|
const el=d.getElementById('mcp-indicator');
|
||||||
|
if(el)el.remove();
|
||||||
|
}catch(e){}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Auto-initialize with final error boundary
|
||||||
|
init().catch(e=>c.warn('[MCP] Voice init failed:',e));
|
||||||
|
c.log('[MCP] Voice collaboration loaded safely');
|
||||||
|
|
||||||
|
}catch(globalError){
|
||||||
|
// Ultimate safety net - never let this script break the page
|
||||||
|
console.warn('[MCP] Voice API failed to load:',globalError);
|
||||||
|
window.mcpNotify={info:()=>{},success:()=>{},warning:()=>{},error:()=>{},speak:()=>{}};
|
||||||
|
window.mcpPrompt=()=>Promise.resolve('');
|
||||||
|
window.mcpInspector={active:0,start:()=>{},stop:()=>{}};
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
`;
|
||||||
|
}
|
||||||
@ -31,6 +31,7 @@ import requests from './tools/requests.js';
|
|||||||
import snapshot from './tools/snapshot.js';
|
import snapshot from './tools/snapshot.js';
|
||||||
import tabs from './tools/tabs.js';
|
import tabs from './tools/tabs.js';
|
||||||
import screenshot from './tools/screenshot.js';
|
import screenshot from './tools/screenshot.js';
|
||||||
|
import themeManagement from './tools/themeManagement.js';
|
||||||
import video from './tools/video.js';
|
import video from './tools/video.js';
|
||||||
import wait from './tools/wait.js';
|
import wait from './tools/wait.js';
|
||||||
import mouse from './tools/mouse.js';
|
import mouse from './tools/mouse.js';
|
||||||
@ -57,6 +58,7 @@ export const allTools: Tool<any>[] = [
|
|||||||
...screenshot,
|
...screenshot,
|
||||||
...snapshot,
|
...snapshot,
|
||||||
...tabs,
|
...tabs,
|
||||||
|
...themeManagement,
|
||||||
...video,
|
...video,
|
||||||
...wait,
|
...wait,
|
||||||
];
|
];
|
||||||
|
|||||||
@ -26,9 +26,47 @@ import { z } from 'zod';
|
|||||||
import { defineTool } from './tool.js';
|
import { defineTool } from './tool.js';
|
||||||
import type { Context } from '../context.js';
|
import type { Context } from '../context.js';
|
||||||
import type { Response } from '../response.js';
|
import type { Response } from '../response.js';
|
||||||
|
import { generateVoiceCollaborationAPI } from '../collaboration/voiceAPI.js';
|
||||||
|
|
||||||
const testDebug = debug('pw:mcp:tools:injection');
|
const testDebug = debug('pw:mcp:tools:injection');
|
||||||
|
|
||||||
|
// Direct voice API injection that bypasses wrapper issues
|
||||||
|
export async function injectVoiceAPIDirectly(context: Context, voiceScript: string): Promise<void> {
|
||||||
|
const currentTab = context.currentTab();
|
||||||
|
if (!currentTab) return;
|
||||||
|
|
||||||
|
// Custom injection that preserves variable scoping and avoids template literal issues
|
||||||
|
const wrappedVoiceScript = `
|
||||||
|
(function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// Prevent double injection
|
||||||
|
if (window.mcpVoiceLoaded) {
|
||||||
|
console.log('[MCP] Voice API already loaded, skipping');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
${voiceScript}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[MCP] Voice API injection failed:', error);
|
||||||
|
// Provide minimal fallback functions
|
||||||
|
window.mcpNotify = {
|
||||||
|
info: (msg) => console.log('[MCP Info]', msg || ''),
|
||||||
|
success: (msg) => console.log('[MCP Success]', msg || ''),
|
||||||
|
warning: (msg) => console.warn('[MCP Warning]', msg || ''),
|
||||||
|
error: (msg) => console.error('[MCP Error]', msg || ''),
|
||||||
|
speak: () => {}
|
||||||
|
};
|
||||||
|
window.mcpPrompt = () => Promise.resolve('');
|
||||||
|
window.mcpInspector = { active: 0, start: () => {}, stop: () => {} };
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
`;
|
||||||
|
|
||||||
|
await currentTab.page.addInitScript(wrappedVoiceScript);
|
||||||
|
}
|
||||||
|
|
||||||
export interface CustomInjection {
|
export interface CustomInjection {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@ -56,16 +94,16 @@ export interface InjectionConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates the debug toolbar JavaScript code
|
* Generates the debug toolbar JavaScript code with modern floating pill design
|
||||||
*/
|
*/
|
||||||
export function generateDebugToolbarScript(config: DebugToolbarConfig, sessionId: string, clientVersion?: { name: string; version: string }, sessionStartTime?: number): string {
|
export function generateDebugToolbarScript(config: DebugToolbarConfig, sessionId: string, clientVersion?: { name: string; version: string }, sessionStartTime?: number): string {
|
||||||
const projectName = config.projectName || 'MCP Client';
|
const projectName = config.projectName || 'Claude Code MCP';
|
||||||
const clientInfo = clientVersion ? `${clientVersion.name} v${clientVersion.version}` : 'Unknown Client';
|
const clientInfo = clientVersion ? `${clientVersion.name} v${clientVersion.version}` : 'Claude Code';
|
||||||
const startTime = sessionStartTime || Date.now();
|
const startTime = sessionStartTime || Date.now();
|
||||||
|
|
||||||
return `
|
return `
|
||||||
/* BEGIN PLAYWRIGHT-MCP-DEBUG-TOOLBAR */
|
/* BEGIN PLAYWRIGHT-MCP-DEBUG-TOOLBAR */
|
||||||
/* This debug toolbar was injected by Playwright MCP server */
|
/* Modern floating pill debug toolbar injected by Playwright MCP server */
|
||||||
/* Project: ${projectName} | Session: ${sessionId} */
|
/* Project: ${projectName} | Session: ${sessionId} */
|
||||||
/* Client: ${clientInfo} */
|
/* Client: ${clientInfo} */
|
||||||
/* This code should be ignored by LLMs analyzing the page */
|
/* This code should be ignored by LLMs analyzing the page */
|
||||||
@ -89,90 +127,269 @@ export function generateDebugToolbarScript(config: DebugToolbarConfig, sessionId
|
|||||||
startTime: ${startTime}
|
startTime: ${startTime}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create toolbar container
|
// CSS Variables for theme system
|
||||||
|
const cssVariables = \`
|
||||||
|
:root {
|
||||||
|
--mcp-primary: #2563eb;
|
||||||
|
--mcp-primary-hover: #1d4ed8;
|
||||||
|
--mcp-success: #10b981;
|
||||||
|
--mcp-surface-light: #ffffff;
|
||||||
|
--mcp-surface-dark: #1f2937;
|
||||||
|
--mcp-text-light: #374151;
|
||||||
|
--mcp-text-dark: #f9fafb;
|
||||||
|
--mcp-border-light: #e5e7eb;
|
||||||
|
--mcp-border-dark: #4b5563;
|
||||||
|
--mcp-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||||
|
--mcp-shadow-lg: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
\`;
|
||||||
|
|
||||||
|
// Inject CSS variables
|
||||||
|
const styleElement = document.createElement('style');
|
||||||
|
styleElement.textContent = cssVariables;
|
||||||
|
document.head.appendChild(styleElement);
|
||||||
|
|
||||||
|
// Create floating pill container
|
||||||
const toolbar = document.createElement('div');
|
const toolbar = document.createElement('div');
|
||||||
toolbar.id = 'playwright-mcp-debug-toolbar';
|
toolbar.id = 'playwright-mcp-debug-toolbar';
|
||||||
toolbar.className = 'playwright-mcp-debug-toolbar';
|
toolbar.className = 'playwright-mcp-debug-toolbar';
|
||||||
|
|
||||||
// Position styles
|
// Position calculations
|
||||||
const positions = {
|
const positions = {
|
||||||
'top-left': { top: '10px', left: '10px' },
|
'top-left': { top: '16px', left: '16px', right: 'auto', bottom: 'auto' },
|
||||||
'top-right': { top: '10px', right: '10px' },
|
'top-right': { top: '16px', right: '16px', left: 'auto', bottom: 'auto' },
|
||||||
'bottom-left': { bottom: '10px', left: '10px' },
|
'bottom-left': { bottom: '16px', left: '16px', right: 'auto', top: 'auto' },
|
||||||
'bottom-right': { bottom: '10px', right: '10px' }
|
'bottom-right': { bottom: '16px', right: '16px', left: 'auto', top: 'auto' }
|
||||||
};
|
};
|
||||||
|
|
||||||
const pos = positions[toolbarConfig.position] || positions['top-right'];
|
const pos = positions[toolbarConfig.position] || positions['top-right'];
|
||||||
|
|
||||||
// Theme colors
|
// Theme-based styling
|
||||||
|
const getThemeStyles = (theme, minimized) => {
|
||||||
const themes = {
|
const themes = {
|
||||||
light: { bg: 'rgba(255,255,255,0.95)', text: '#333', border: '#ccc' },
|
light: {
|
||||||
dark: { bg: 'rgba(45,45,45,0.95)', text: '#fff', border: '#666' },
|
background: 'var(--mcp-surface-light)',
|
||||||
transparent: { bg: 'rgba(0,0,0,0.7)', text: '#fff', border: 'rgba(255,255,255,0.3)' }
|
color: 'var(--mcp-text-light)',
|
||||||
|
border: '1px solid var(--mcp-border-light)',
|
||||||
|
shadow: 'var(--mcp-shadow)'
|
||||||
|
},
|
||||||
|
dark: {
|
||||||
|
background: 'var(--mcp-surface-dark)',
|
||||||
|
color: 'var(--mcp-text-dark)',
|
||||||
|
border: '1px solid var(--mcp-border-dark)',
|
||||||
|
shadow: 'var(--mcp-shadow)'
|
||||||
|
},
|
||||||
|
transparent: {
|
||||||
|
background: 'rgba(15, 23, 42, 0.95)',
|
||||||
|
color: '#f1f5f9',
|
||||||
|
border: '1px solid rgba(148, 163, 184, 0.2)',
|
||||||
|
shadow: 'var(--mcp-shadow-lg)'
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const theme = themes[toolbarConfig.theme] || themes.dark;
|
const themeData = themes[theme] || themes.dark;
|
||||||
|
|
||||||
// Base styles
|
return \`
|
||||||
toolbar.style.cssText = \`
|
|
||||||
position: fixed;
|
position: fixed;
|
||||||
\${Object.entries(pos).map(([k,v]) => k + ':' + v).join(';')};
|
\${Object.entries(pos).map(([k,v]) => \`\${k}: \${v}\`).join('; ')};
|
||||||
background: \${theme.bg};
|
background: \${themeData.background};
|
||||||
color: \${theme.text};
|
color: \${themeData.color};
|
||||||
border: 1px solid \${theme.border};
|
border: \${themeData.border};
|
||||||
border-radius: 6px;
|
border-radius: \${minimized ? '24px' : '12px'};
|
||||||
padding: 8px 12px;
|
padding: \${minimized ? '8px 12px' : '12px 16px'};
|
||||||
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
font-size: 12px;
|
font-size: \${minimized ? '12px' : '13px'};
|
||||||
|
font-weight: 500;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
z-index: 999999;
|
z-index: 2147483647;
|
||||||
opacity: \${toolbarConfig.opacity};
|
opacity: \${toolbarConfig.opacity || 0.95};
|
||||||
cursor: move;
|
backdrop-filter: blur(12px);
|
||||||
|
-webkit-backdrop-filter: blur(12px);
|
||||||
|
box-shadow: \${themeData.shadow};
|
||||||
|
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
user-select: none;
|
user-select: none;
|
||||||
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
cursor: grab;
|
||||||
min-width: 150px;
|
max-width: \${minimized ? '200px' : '320px'};
|
||||||
max-width: 300px;
|
min-width: \${minimized ? 'auto' : '240px'};
|
||||||
\`;
|
\`;
|
||||||
|
};
|
||||||
|
|
||||||
// Create content
|
// Hover enhancement styles
|
||||||
function updateToolbarContent() {
|
const addHoverStyles = () => {
|
||||||
const uptime = Math.floor((Date.now() - sessionInfo.startTime) / 1000);
|
const hoverStyleElement = document.createElement('style');
|
||||||
|
hoverStyleElement.id = 'mcp-toolbar-hover-styles';
|
||||||
|
hoverStyleElement.textContent = \`
|
||||||
|
#playwright-mcp-debug-toolbar:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: var(--mcp-shadow-lg);
|
||||||
|
opacity: 1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#playwright-mcp-debug-toolbar:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
transform: translateY(0px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcp-toolbar-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
font-size: 12px;
|
||||||
|
color: inherit;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcp-toolbar-btn:hover {
|
||||||
|
opacity: 1;
|
||||||
|
background: rgba(99, 102, 241, 0.1);
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcp-status-indicator {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--mcp-success);
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 8px;
|
||||||
|
box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.2);
|
||||||
|
animation: pulse 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% { box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.2); }
|
||||||
|
50% { box-shadow: 0 0 0 4px rgba(16, 185, 129, 0.1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcp-session-details {
|
||||||
|
font-size: 11px;
|
||||||
|
opacity: 0.8;
|
||||||
|
line-height: 1.3;
|
||||||
|
margin-top: 8px;
|
||||||
|
padding-top: 8px;
|
||||||
|
border-top: 1px solid rgba(148, 163, 184, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcp-session-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcp-session-label {
|
||||||
|
opacity: 0.7;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcp-session-value {
|
||||||
|
font-weight: 500;
|
||||||
|
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
#playwright-mcp-debug-toolbar {
|
||||||
|
font-size: 11px;
|
||||||
|
min-width: 200px;
|
||||||
|
max-width: 280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcp-session-details {
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
\`;
|
||||||
|
document.head.appendChild(hoverStyleElement);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add hover styles
|
||||||
|
addHoverStyles();
|
||||||
|
|
||||||
|
// Content generation functions
|
||||||
|
function formatUptime(startTime) {
|
||||||
|
const uptime = Math.floor((Date.now() - startTime) / 1000);
|
||||||
const hours = Math.floor(uptime / 3600);
|
const hours = Math.floor(uptime / 3600);
|
||||||
const minutes = Math.floor((uptime % 3600) / 60);
|
const minutes = Math.floor((uptime % 3600) / 60);
|
||||||
const seconds = uptime % 60;
|
const seconds = uptime % 60;
|
||||||
const uptimeStr = hours > 0 ?
|
|
||||||
\`\${hours}h \${minutes}m \${seconds}s\` :
|
|
||||||
minutes > 0 ? \`\${minutes}m \${seconds}s\` : \`\${seconds}s\`;
|
|
||||||
|
|
||||||
if (toolbarConfig.minimized) {
|
if (hours > 0) return \`\${hours}h \${minutes}m\`;
|
||||||
toolbar.innerHTML = \`
|
if (minutes > 0) return \`\${minutes}m \${seconds}s\`;
|
||||||
<div style="display: flex; align-items: center; justify-content: space-between;">
|
return \`\${seconds}s\`;
|
||||||
<span style="font-weight: bold; color: #4CAF50;">●</span>
|
}
|
||||||
<span style="margin: 0 8px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
|
|
||||||
|
function generateMinimizedContent() {
|
||||||
|
return \`
|
||||||
|
<div style="display: flex; align-items: center; justify-content: space-between; gap: 8px;">
|
||||||
|
<div style="display: flex; align-items: center; flex: 1; min-width: 0;">
|
||||||
|
<span class="mcp-status-indicator"></span>
|
||||||
|
<span style="font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
|
||||||
\${sessionInfo.project}
|
\${sessionInfo.project}
|
||||||
</span>
|
</span>
|
||||||
<span style="cursor: pointer; opacity: 0.7; hover: opacity: 1;" onclick="this.parentNode.parentNode.playwrightToggle()">⊞</span>
|
</div>
|
||||||
|
<button class="mcp-toolbar-btn" onclick="this.closest('#playwright-mcp-debug-toolbar').playwrightToggle()" title="Expand details">
|
||||||
|
⊞
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
\`;
|
\`;
|
||||||
} else {
|
}
|
||||||
toolbar.innerHTML = \`
|
|
||||||
<div style="margin-bottom: 4px; display: flex; align-items: center; justify-content: space-between;">
|
function generateExpandedContent() {
|
||||||
<div style="display: flex; align-items: center;">
|
const uptimeStr = formatUptime(sessionInfo.startTime);
|
||||||
<span style="color: #4CAF50; margin-right: 6px;">●</span>
|
const shortSessionId = sessionInfo.id.substring(0, 8);
|
||||||
<strong>\${sessionInfo.project}</strong>
|
const hostname = window.location.hostname || 'local';
|
||||||
|
|
||||||
|
return \`
|
||||||
|
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: \${toolbarConfig.showDetails ? '0px' : '0px'};">
|
||||||
|
<div style="display: flex; align-items: center; flex: 1; min-width: 0;">
|
||||||
|
<span class="mcp-status-indicator"></span>
|
||||||
|
<span style="font-weight: 600; font-size: 14px;">
|
||||||
|
\${sessionInfo.project}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span style="cursor: pointer; opacity: 0.7; hover: opacity: 1;" onclick="this.parentNode.parentNode.playwrightToggle()">⊟</span>
|
<button class="mcp-toolbar-btn" onclick="this.closest('#playwright-mcp-debug-toolbar').playwrightToggle()" title="Minimize">
|
||||||
|
⊟
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
\${toolbarConfig.showDetails ? \`
|
\${toolbarConfig.showDetails ? \`
|
||||||
<div style="font-size: 10px; opacity: 0.8; line-height: 1.2;">
|
<div class="mcp-session-details">
|
||||||
<div>Session: \${sessionInfo.id.substring(0, 12)}...</div>
|
<div class="mcp-session-row">
|
||||||
<div>Client: \${sessionInfo.client}</div>
|
<span class="mcp-session-label">Session:</span>
|
||||||
<div>Uptime: \${uptimeStr}</div>
|
<span class="mcp-session-value">\${shortSessionId}</span>
|
||||||
<div>URL: \${window.location.hostname}</div>
|
</div>
|
||||||
|
<div class="mcp-session-row">
|
||||||
|
<span class="mcp-session-label">Client:</span>
|
||||||
|
<span class="mcp-session-value">\${sessionInfo.client}</span>
|
||||||
|
</div>
|
||||||
|
<div class="mcp-session-row">
|
||||||
|
<span class="mcp-session-label">Uptime:</span>
|
||||||
|
<span class="mcp-session-value">\${uptimeStr}</span>
|
||||||
|
</div>
|
||||||
|
<div class="mcp-session-row">
|
||||||
|
<span class="mcp-session-label">Host:</span>
|
||||||
|
<span class="mcp-session-value">\${hostname}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
\` : ''}
|
\` : ''}
|
||||||
\`;
|
\`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update toolbar content and styling
|
||||||
|
function updateToolbarContent() {
|
||||||
|
const isMinimized = toolbarConfig.minimized;
|
||||||
|
toolbar.style.cssText = getThemeStyles(toolbarConfig.theme, isMinimized);
|
||||||
|
|
||||||
|
if (isMinimized) {
|
||||||
|
toolbar.innerHTML = generateMinimizedContent();
|
||||||
|
} else {
|
||||||
|
toolbar.innerHTML = generateExpandedContent();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Toggle function
|
// Toggle function
|
||||||
@ -181,43 +398,88 @@ export function generateDebugToolbarScript(config: DebugToolbarConfig, sessionId
|
|||||||
updateToolbarContent();
|
updateToolbarContent();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Dragging functionality
|
// Enhanced dragging functionality
|
||||||
let isDragging = false;
|
let isDragging = false;
|
||||||
let dragOffset = { x: 0, y: 0 };
|
let dragOffset = { x: 0, y: 0 };
|
||||||
|
let dragStartTime = 0;
|
||||||
|
|
||||||
toolbar.addEventListener('mousedown', function(e) {
|
toolbar.addEventListener('mousedown', function(e) {
|
||||||
|
// Don't drag if clicking on button
|
||||||
|
if (e.target.classList.contains('mcp-toolbar-btn')) return;
|
||||||
|
|
||||||
isDragging = true;
|
isDragging = true;
|
||||||
dragOffset.x = e.clientX - toolbar.offsetLeft;
|
dragStartTime = Date.now();
|
||||||
dragOffset.y = e.clientY - toolbar.offsetTop;
|
dragOffset.x = e.clientX - toolbar.getBoundingClientRect().left;
|
||||||
|
dragOffset.y = e.clientY - toolbar.getBoundingClientRect().top;
|
||||||
toolbar.style.cursor = 'grabbing';
|
toolbar.style.cursor = 'grabbing';
|
||||||
|
toolbar.style.transform = 'translateY(0px)';
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener('mousemove', function(e) {
|
document.addEventListener('mousemove', function(e) {
|
||||||
if (isDragging) {
|
if (isDragging) {
|
||||||
toolbar.style.left = (e.clientX - dragOffset.x) + 'px';
|
const newLeft = e.clientX - dragOffset.x;
|
||||||
toolbar.style.top = (e.clientY - dragOffset.y) + 'px';
|
const newTop = e.clientY - dragOffset.y;
|
||||||
// Remove position properties when dragging
|
|
||||||
|
// Constrain to viewport
|
||||||
|
const maxLeft = window.innerWidth - toolbar.offsetWidth - 16;
|
||||||
|
const maxTop = window.innerHeight - toolbar.offsetHeight - 16;
|
||||||
|
|
||||||
|
toolbar.style.left = Math.max(16, Math.min(maxLeft, newLeft)) + 'px';
|
||||||
|
toolbar.style.top = Math.max(16, Math.min(maxTop, newTop)) + 'px';
|
||||||
toolbar.style.right = 'auto';
|
toolbar.style.right = 'auto';
|
||||||
toolbar.style.bottom = 'auto';
|
toolbar.style.bottom = 'auto';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener('mouseup', function() {
|
document.addEventListener('mouseup', function(e) {
|
||||||
if (isDragging) {
|
if (isDragging) {
|
||||||
isDragging = false;
|
isDragging = false;
|
||||||
toolbar.style.cursor = 'move';
|
toolbar.style.cursor = 'grab';
|
||||||
|
|
||||||
|
// If it was a quick click (not a drag), treat as toggle
|
||||||
|
const dragDuration = Date.now() - dragStartTime;
|
||||||
|
const wasQuickClick = dragDuration < 200;
|
||||||
|
const dragDistance = Math.sqrt(
|
||||||
|
Math.pow(e.clientX - (toolbar.getBoundingClientRect().left + dragOffset.x), 2) +
|
||||||
|
Math.pow(e.clientY - (toolbar.getBoundingClientRect().top + dragOffset.y), 2)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (wasQuickClick && dragDistance < 5) {
|
||||||
|
toolbar.playwrightToggle();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update content initially and every second
|
// Keyboard accessibility
|
||||||
|
toolbar.addEventListener('keydown', function(e) {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
toolbar.playwrightToggle();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Make focusable for accessibility
|
||||||
|
toolbar.setAttribute('tabindex', '0');
|
||||||
|
toolbar.setAttribute('role', 'application');
|
||||||
|
toolbar.setAttribute('aria-label', \`MCP Debug Toolbar for \${sessionInfo.project}\`);
|
||||||
|
|
||||||
|
// Update content initially and every 30 seconds (reduced frequency)
|
||||||
updateToolbarContent();
|
updateToolbarContent();
|
||||||
setInterval(updateToolbarContent, 1000);
|
const updateInterval = setInterval(updateToolbarContent, 30000);
|
||||||
|
|
||||||
|
// Cleanup function
|
||||||
|
toolbar.playwrightCleanup = function() {
|
||||||
|
clearInterval(updateInterval);
|
||||||
|
const hoverStyles = document.getElementById('mcp-toolbar-hover-styles');
|
||||||
|
if (hoverStyles) hoverStyles.remove();
|
||||||
|
toolbar.remove();
|
||||||
|
};
|
||||||
|
|
||||||
// Add to page
|
// Add to page
|
||||||
document.body.appendChild(toolbar);
|
document.body.appendChild(toolbar);
|
||||||
|
|
||||||
console.log(\`[Playwright MCP] Debug toolbar injected - Project: \${sessionInfo.project}, Session: \${sessionInfo.id}\`);
|
console.log(\`[Playwright MCP] Modern debug toolbar injected - Project: \${sessionInfo.project}, Session: \${sessionInfo.id}\`);
|
||||||
})();
|
})();
|
||||||
/* END PLAYWRIGHT-MCP-DEBUG-TOOLBAR */
|
/* END PLAYWRIGHT-MCP-DEBUG-TOOLBAR */
|
||||||
`;
|
`;
|
||||||
@ -298,12 +560,12 @@ export function generateInjectionScript(wrappedCode: string): string {
|
|||||||
|
|
||||||
// Tool schemas
|
// Tool schemas
|
||||||
const enableDebugToolbarSchema = z.object({
|
const enableDebugToolbarSchema = z.object({
|
||||||
projectName: z.string().optional().describe('Name of your project/client to display in the toolbar'),
|
projectName: z.string().optional().describe('Name of your project/client to display in the floating pill toolbar'),
|
||||||
position: z.enum(['top-left', 'top-right', 'bottom-left', 'bottom-right']).optional().describe('Position of the toolbar on screen'),
|
position: z.enum(['top-left', 'top-right', 'bottom-left', 'bottom-right']).optional().describe('Position of the floating pill on screen (default: top-right)'),
|
||||||
theme: z.enum(['light', 'dark', 'transparent']).optional().describe('Visual theme for the toolbar'),
|
theme: z.enum(['light', 'dark', 'transparent']).optional().describe('Visual theme: light (white), dark (gray), transparent (glass effect)'),
|
||||||
minimized: z.boolean().optional().describe('Start toolbar in minimized state'),
|
minimized: z.boolean().optional().describe('Start in compact pill mode (default: false)'),
|
||||||
showDetails: z.boolean().optional().describe('Show session details in expanded view'),
|
showDetails: z.boolean().optional().describe('Show session details when expanded (default: true)'),
|
||||||
opacity: z.number().min(0.1).max(1.0).optional().describe('Toolbar opacity')
|
opacity: z.number().min(0.1).max(1.0).optional().describe('Toolbar opacity 0.1-1.0 (default: 0.95)')
|
||||||
});
|
});
|
||||||
|
|
||||||
const injectCustomCodeSchema = z.object({
|
const injectCustomCodeSchema = z.object({
|
||||||
@ -314,6 +576,22 @@ const injectCustomCodeSchema = z.object({
|
|||||||
autoInject: z.boolean().optional().describe('Automatically inject on every new page')
|
autoInject: z.boolean().optional().describe('Automatically inject on every new page')
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const enableVoiceCollaborationSchema = z.object({
|
||||||
|
enabled: z.boolean().optional().describe('Enable voice collaboration features (default: true)'),
|
||||||
|
autoInitialize: z.boolean().optional().describe('Automatically initialize voice on page load (default: true)'),
|
||||||
|
voiceOptions: z.object({
|
||||||
|
rate: z.number().min(0.1).max(10).optional().describe('Speech rate (0.1-10, default: 1.0)'),
|
||||||
|
pitch: z.number().min(0).max(2).optional().describe('Speech pitch (0-2, default: 1.0)'),
|
||||||
|
volume: z.number().min(0).max(1).optional().describe('Speech volume (0-1, default: 1.0)'),
|
||||||
|
lang: z.string().optional().describe('Language code (default: en-US)')
|
||||||
|
}).optional().describe('Voice synthesis options'),
|
||||||
|
listenOptions: z.object({
|
||||||
|
timeout: z.number().min(1000).max(60000).optional().describe('Voice input timeout in milliseconds (default: 10000)'),
|
||||||
|
lang: z.string().optional().describe('Speech recognition language (default: en-US)'),
|
||||||
|
continuous: z.boolean().optional().describe('Keep listening after first result (default: false)')
|
||||||
|
}).optional().describe('Voice recognition options')
|
||||||
|
});
|
||||||
|
|
||||||
const clearInjectionsSchema = z.object({
|
const clearInjectionsSchema = z.object({
|
||||||
includeToolbar: z.boolean().optional().describe('Also disable debug toolbar')
|
includeToolbar: z.boolean().optional().describe('Also disable debug toolbar')
|
||||||
});
|
});
|
||||||
@ -323,8 +601,8 @@ const enableDebugToolbar = defineTool({
|
|||||||
capability: 'core',
|
capability: 'core',
|
||||||
schema: {
|
schema: {
|
||||||
name: 'browser_enable_debug_toolbar',
|
name: 'browser_enable_debug_toolbar',
|
||||||
title: 'Enable Debug Toolbar',
|
title: 'Enable Modern Debug Toolbar',
|
||||||
description: 'Enable the debug toolbar to identify which MCP client is controlling the browser',
|
description: 'Enable a modern floating pill toolbar with excellent contrast and professional design to identify which MCP client controls the browser',
|
||||||
inputSchema: enableDebugToolbarSchema,
|
inputSchema: enableDebugToolbarSchema,
|
||||||
type: 'destructive',
|
type: 'destructive',
|
||||||
},
|
},
|
||||||
@ -333,12 +611,12 @@ const enableDebugToolbar = defineTool({
|
|||||||
|
|
||||||
const config: DebugToolbarConfig = {
|
const config: DebugToolbarConfig = {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
projectName: params.projectName || 'MCP Client',
|
projectName: params.projectName || 'Claude Code MCP',
|
||||||
position: params.position || 'top-right',
|
position: params.position || 'top-right',
|
||||||
theme: params.theme || 'dark',
|
theme: params.theme || 'dark',
|
||||||
minimized: params.minimized || false,
|
minimized: params.minimized || false,
|
||||||
showDetails: params.showDetails !== false,
|
showDetails: params.showDetails !== false,
|
||||||
opacity: params.opacity || 0.9
|
opacity: params.opacity || 0.95
|
||||||
};
|
};
|
||||||
|
|
||||||
// Store config in context
|
// Store config in context
|
||||||
@ -368,10 +646,11 @@ const enableDebugToolbar = defineTool({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const resultMessage = `Debug toolbar enabled for project "${config.projectName}"`;
|
const resultMessage = `Modern floating pill toolbar enabled for project "${config.projectName}"`;
|
||||||
response.addResult(resultMessage);
|
response.addResult(resultMessage);
|
||||||
|
response.addResult(`Theme: ${config.theme} | Position: ${config.position} | Opacity: ${config.opacity}`);
|
||||||
response.addResult(`Session ID: ${context.sessionId}`);
|
response.addResult(`Session ID: ${context.sessionId}`);
|
||||||
response.addResult(`Auto-injection enabled for new pages`);
|
response.addResult(`Features: Draggable, expandable, high-contrast design with accessibility support`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -380,7 +659,21 @@ const injectCustomCode = defineTool({
|
|||||||
schema: {
|
schema: {
|
||||||
name: 'browser_inject_custom_code',
|
name: 'browser_inject_custom_code',
|
||||||
title: 'Inject Custom Code',
|
title: 'Inject Custom Code',
|
||||||
description: 'Inject custom JavaScript or CSS code into all pages in the current session',
|
description: `Inject custom JavaScript or CSS code into all pages in the current session
|
||||||
|
|
||||||
|
🤖 COLLABORATION API AVAILABLE:
|
||||||
|
Models can inject JavaScript that communicates directly with users:
|
||||||
|
• mcpNotify.info('message') - Send info to user
|
||||||
|
• mcpNotify.success('completed!') - Show success
|
||||||
|
• mcpNotify.warning('be careful') - Display warnings
|
||||||
|
• mcpNotify.error('something failed') - Show errors
|
||||||
|
• await mcpPrompt('Shall I proceed?') - Get user confirmation
|
||||||
|
• mcpInspector.start('Click the login button', callback) - Interactive element selection
|
||||||
|
|
||||||
|
When elements are ambiguous or actions need confirmation, use these functions
|
||||||
|
to collaborate with the user for better automation results.
|
||||||
|
|
||||||
|
Full API: See MODEL-COLLABORATION-API.md`,
|
||||||
inputSchema: injectCustomCodeSchema,
|
inputSchema: injectCustomCodeSchema,
|
||||||
type: 'destructive',
|
type: 'destructive',
|
||||||
},
|
},
|
||||||
@ -511,6 +804,126 @@ const disableDebugToolbar = defineTool({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const enableVoiceCollaboration = defineTool({
|
||||||
|
capability: 'core',
|
||||||
|
schema: {
|
||||||
|
name: 'browser_enable_voice_collaboration',
|
||||||
|
title: 'Enable Voice Collaboration',
|
||||||
|
description: `🎤 REVOLUTIONARY: Enable conversational browser automation with voice communication!
|
||||||
|
|
||||||
|
**Transform browser automation into natural conversation:**
|
||||||
|
• AI speaks to you in real-time during automation
|
||||||
|
• Respond with your voice instead of typing
|
||||||
|
• Interactive decision-making during tasks
|
||||||
|
• "Hey Claude, what should I click?" → AI guides you with voice
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
• Native browser Web Speech API (no external services)
|
||||||
|
• Automatic microphone permission handling
|
||||||
|
• Intelligent fallbacks when voice unavailable
|
||||||
|
• Real-time collaboration during automation tasks
|
||||||
|
|
||||||
|
**Example Usage:**
|
||||||
|
AI: "I found a login form. What credentials should I use?" 🗣️
|
||||||
|
You: "Use my work email and check password manager" 🎤
|
||||||
|
AI: "Perfect! Logging you in now..." 🗣️
|
||||||
|
|
||||||
|
This is the FIRST conversational browser automation MCP server!`,
|
||||||
|
inputSchema: enableVoiceCollaborationSchema,
|
||||||
|
type: 'destructive',
|
||||||
|
},
|
||||||
|
handle: async (context: Context, params: z.output<typeof enableVoiceCollaborationSchema>, response: Response) => {
|
||||||
|
testDebug('Enabling voice collaboration with params:', params);
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
enabled: params.enabled !== false,
|
||||||
|
autoInitialize: params.autoInitialize !== false,
|
||||||
|
voiceOptions: {
|
||||||
|
rate: params.voiceOptions?.rate || 1.0,
|
||||||
|
pitch: params.voiceOptions?.pitch || 1.0,
|
||||||
|
volume: params.voiceOptions?.volume || 1.0,
|
||||||
|
lang: params.voiceOptions?.lang || 'en-US'
|
||||||
|
},
|
||||||
|
listenOptions: {
|
||||||
|
timeout: params.listenOptions?.timeout || 10000,
|
||||||
|
lang: params.listenOptions?.lang || 'en-US',
|
||||||
|
continuous: params.listenOptions?.continuous || false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Generate the voice collaboration API injection
|
||||||
|
const voiceAPIScript = generateVoiceCollaborationAPI();
|
||||||
|
|
||||||
|
// Create injection object
|
||||||
|
const injection: CustomInjection = {
|
||||||
|
id: `voice_collaboration_${Date.now()}`,
|
||||||
|
name: 'voice-collaboration',
|
||||||
|
type: 'javascript',
|
||||||
|
code: voiceAPIScript,
|
||||||
|
enabled: config.enabled,
|
||||||
|
persistent: true,
|
||||||
|
autoInject: true
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize injection config if needed
|
||||||
|
if (!context.injectionConfig) {
|
||||||
|
context.injectionConfig = {
|
||||||
|
debugToolbar: { enabled: false, minimized: false, showDetails: true, position: 'top-right', theme: 'dark', opacity: 0.9 },
|
||||||
|
customInjections: [],
|
||||||
|
enabled: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove any existing voice collaboration injection
|
||||||
|
context.injectionConfig.customInjections = context.injectionConfig.customInjections.filter(
|
||||||
|
inj => inj.name !== 'voice-collaboration'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add new voice collaboration injection
|
||||||
|
context.injectionConfig.customInjections.push(injection);
|
||||||
|
|
||||||
|
// Use direct injection method to avoid template literal and timing issues
|
||||||
|
if (config.enabled) {
|
||||||
|
try {
|
||||||
|
await injectVoiceAPIDirectly(context, voiceAPIScript);
|
||||||
|
testDebug('Voice collaboration API injected directly via addInitScript');
|
||||||
|
} catch (error) {
|
||||||
|
testDebug('Error injecting voice collaboration via direct method:', error);
|
||||||
|
|
||||||
|
// Fallback: try basic addInitScript only (no evaluate)
|
||||||
|
const currentTab = context.currentTab();
|
||||||
|
if (currentTab) {
|
||||||
|
try {
|
||||||
|
await currentTab.page.addInitScript(`
|
||||||
|
(function(){
|
||||||
|
try {
|
||||||
|
${voiceAPIScript}
|
||||||
|
} catch(e) {
|
||||||
|
console.warn('[MCP] Voice API fallback failed:', e);
|
||||||
|
window.mcpNotify = {info:()=>{}, success:()=>{}, warning:()=>{}, error:()=>{}, speak:()=>{}};
|
||||||
|
window.mcpPrompt = () => Promise.resolve('');
|
||||||
|
window.mcpInspector = {active:0, start:()=>{}, stop:()=>{}};
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
`);
|
||||||
|
testDebug('Voice collaboration API injected via fallback method');
|
||||||
|
} catch (fallbackError) {
|
||||||
|
testDebug('Fallback injection also failed:', fallbackError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resultMessage = `🎤 Voice collaboration enabled!
|
||||||
|
• Speech rate: ${config.voiceOptions.rate}x, pitch: ${config.voiceOptions.pitch}
|
||||||
|
• Recognition timeout: ${config.listenOptions.timeout}ms, language: ${config.voiceOptions.lang}
|
||||||
|
• Try: mcpNotify.speak("Hello!"), mcpPrompt("Search for?", {useVoice:true})
|
||||||
|
🚀 First conversational browser automation MCP server is now active!`;
|
||||||
|
|
||||||
|
response.addResult(resultMessage);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const clearInjections = defineTool({
|
const clearInjections = defineTool({
|
||||||
capability: 'core',
|
capability: 'core',
|
||||||
schema: {
|
schema: {
|
||||||
@ -558,5 +971,6 @@ export default [
|
|||||||
injectCustomCode,
|
injectCustomCode,
|
||||||
listInjections,
|
listInjections,
|
||||||
disableDebugToolbar,
|
disableDebugToolbar,
|
||||||
|
enableVoiceCollaboration,
|
||||||
clearInjections,
|
clearInjections,
|
||||||
];
|
];
|
||||||
|
|||||||
362
src/tools/themeManagement.ts
Normal file
362
src/tools/themeManagement.ts
Normal file
@ -0,0 +1,362 @@
|
|||||||
|
/**
|
||||||
|
* MCP Theme Management Tools
|
||||||
|
* Professional theme system for MCP client identification
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { defineTabTool } from './tool.js';
|
||||||
|
import * as javascript from '../javascript.js';
|
||||||
|
|
||||||
|
// Theme schema definitions
|
||||||
|
const themeVariablesSchema = z.record(z.string()).describe('CSS custom properties for the theme');
|
||||||
|
|
||||||
|
const themeSchema = z.object({
|
||||||
|
id: z.string().describe('Unique theme identifier'),
|
||||||
|
name: z.string().describe('Human-readable theme name'),
|
||||||
|
description: z.string().describe('Theme description'),
|
||||||
|
variables: themeVariablesSchema,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Built-in themes registry
|
||||||
|
const builtInThemes: Record<string, {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
variables: Record<string, string>;
|
||||||
|
}> = {
|
||||||
|
minimal: {
|
||||||
|
id: 'minimal',
|
||||||
|
name: 'Minimal',
|
||||||
|
description: 'Clean, GitHub-style design with excellent readability',
|
||||||
|
variables: {
|
||||||
|
'--mcp-bg': 'rgba(255, 255, 255, 0.95)',
|
||||||
|
'--mcp-color': '#24292f',
|
||||||
|
'--mcp-border': '#d0d7de',
|
||||||
|
'--mcp-shadow': '0 1px 3px rgba(0, 0, 0, 0.1)',
|
||||||
|
'--mcp-radius': '6px',
|
||||||
|
'--mcp-font': '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
|
||||||
|
'--mcp-size': '13px',
|
||||||
|
'--mcp-padding': '8px 12px',
|
||||||
|
'--mcp-status-color': '#2da44e',
|
||||||
|
'--mcp-hover-bg': 'rgba(255, 255, 255, 1)',
|
||||||
|
'--mcp-hover-shadow': '0 3px 8px rgba(0, 0, 0, 0.15)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
corporate: {
|
||||||
|
id: 'corporate',
|
||||||
|
name: 'Corporate',
|
||||||
|
description: 'Professional enterprise design with gradient background',
|
||||||
|
variables: {
|
||||||
|
'--mcp-bg': 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||||
|
'--mcp-color': '#ffffff',
|
||||||
|
'--mcp-border': 'rgba(255, 255, 255, 0.2)',
|
||||||
|
'--mcp-shadow': '0 4px 20px rgba(0, 0, 0, 0.15)',
|
||||||
|
'--mcp-radius': '8px',
|
||||||
|
'--mcp-font': '"Segoe UI", Tahoma, Geneva, Verdana, sans-serif',
|
||||||
|
'--mcp-size': '14px',
|
||||||
|
'--mcp-padding': '10px 16px',
|
||||||
|
'--mcp-status-color': '#4ade80',
|
||||||
|
'--mcp-hover-bg': 'linear-gradient(135deg, #5a67d8 0%, #6b46c1 100%)',
|
||||||
|
'--mcp-hover-shadow': '0 6px 25px rgba(0, 0, 0, 0.25)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
hacker: {
|
||||||
|
id: 'hacker',
|
||||||
|
name: 'Hacker Matrix',
|
||||||
|
description: 'Terminal-style neon green design for cyberpunk aesthetic',
|
||||||
|
variables: {
|
||||||
|
'--mcp-bg': 'linear-gradient(135deg, #000000 0%, #1a1a1a 50%, #0d0d0d 100%)',
|
||||||
|
'--mcp-color': '#00ff41',
|
||||||
|
'--mcp-border': '#00ff41',
|
||||||
|
'--mcp-shadow': '0 0 15px rgba(0, 255, 65, 0.4), 0 0 30px rgba(0, 255, 65, 0.2)',
|
||||||
|
'--mcp-radius': '4px',
|
||||||
|
'--mcp-font': '"Courier New", "Monaco", "Menlo", monospace',
|
||||||
|
'--mcp-size': '12px',
|
||||||
|
'--mcp-padding': '10px 16px',
|
||||||
|
'--mcp-status-color': '#00ff41',
|
||||||
|
'--mcp-hover-bg': 'linear-gradient(135deg, #0a0a0a 0%, #2a2a2a 50%, #1a1a1a 100%)',
|
||||||
|
'--mcp-hover-shadow': '0 0 25px rgba(0, 255, 65, 0.6), 0 0 50px rgba(0, 255, 65, 0.3)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
glass: {
|
||||||
|
id: 'glass',
|
||||||
|
name: 'Glass Morphism',
|
||||||
|
description: 'Modern glass effect with backdrop blur',
|
||||||
|
variables: {
|
||||||
|
'--mcp-bg': 'rgba(255, 255, 255, 0.1)',
|
||||||
|
'--mcp-color': '#374151',
|
||||||
|
'--mcp-border': 'rgba(255, 255, 255, 0.2)',
|
||||||
|
'--mcp-shadow': '0 8px 32px rgba(0, 0, 0, 0.1)',
|
||||||
|
'--mcp-radius': '16px',
|
||||||
|
'--mcp-font': '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
|
||||||
|
'--mcp-size': '13px',
|
||||||
|
'--mcp-padding': '12px 18px',
|
||||||
|
'--mcp-status-color': '#10b981',
|
||||||
|
'--mcp-hover-bg': 'rgba(255, 255, 255, 0.2)',
|
||||||
|
'--mcp-hover-shadow': '0 12px 40px rgba(0, 0, 0, 0.15)',
|
||||||
|
'--mcp-backdrop': 'blur(20px)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
highContrast: {
|
||||||
|
id: 'highContrast',
|
||||||
|
name: 'High Contrast',
|
||||||
|
description: 'Maximum accessibility with WCAG AAA compliance',
|
||||||
|
variables: {
|
||||||
|
'--mcp-bg': '#000000',
|
||||||
|
'--mcp-color': '#ffffff',
|
||||||
|
'--mcp-border': '#ffffff',
|
||||||
|
'--mcp-shadow': '0 2px 8px rgba(255, 255, 255, 0.2)',
|
||||||
|
'--mcp-radius': '4px',
|
||||||
|
'--mcp-font': 'Arial, sans-serif',
|
||||||
|
'--mcp-size': '16px',
|
||||||
|
'--mcp-padding': '12px 16px',
|
||||||
|
'--mcp-status-color': '#ffff00',
|
||||||
|
'--mcp-hover-bg': '#333333',
|
||||||
|
'--mcp-hover-shadow': '0 4px 12px rgba(255, 255, 255, 0.3)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// List available themes
|
||||||
|
const listThemes = defineTabTool({
|
||||||
|
capability: 'core',
|
||||||
|
schema: {
|
||||||
|
name: 'browser_mcp_theme_list',
|
||||||
|
title: 'List MCP themes',
|
||||||
|
description: 'List all available MCP client identification themes',
|
||||||
|
inputSchema: z.object({
|
||||||
|
filter: z.enum(['all', 'builtin', 'custom']).optional().default('all').describe('Filter themes by type'),
|
||||||
|
}),
|
||||||
|
type: 'readOnly',
|
||||||
|
},
|
||||||
|
|
||||||
|
handle: async (tab, params, response) => {
|
||||||
|
const { filter } = params;
|
||||||
|
|
||||||
|
let themes = Object.values(builtInThemes);
|
||||||
|
|
||||||
|
if (filter === 'builtin') {
|
||||||
|
themes = Object.values(builtInThemes);
|
||||||
|
} else if (filter === 'custom') {
|
||||||
|
// In a real implementation, this would fetch custom themes from storage
|
||||||
|
themes = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const themeList = themes.map(theme => ({
|
||||||
|
id: theme.id,
|
||||||
|
name: theme.name,
|
||||||
|
description: theme.description,
|
||||||
|
type: 'builtin'
|
||||||
|
}));
|
||||||
|
|
||||||
|
response.addResult(`Found ${themeList.length} available themes:`);
|
||||||
|
themeList.forEach(theme => {
|
||||||
|
response.addResult(`• **${theme.name}** (${theme.id}): ${theme.description}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
response.addCode(`// List available MCP themes`);
|
||||||
|
response.addCode(`const themes = ${JSON.stringify(themeList, null, 2)};`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set active theme
|
||||||
|
const setTheme = defineTabTool({
|
||||||
|
capability: 'core',
|
||||||
|
schema: {
|
||||||
|
name: 'browser_mcp_theme_set',
|
||||||
|
title: 'Set MCP theme',
|
||||||
|
description: 'Apply a theme to the MCP client identification toolbar',
|
||||||
|
inputSchema: z.object({
|
||||||
|
themeId: z.string().describe('Theme identifier to apply'),
|
||||||
|
persist: z.boolean().optional().default(true).describe('Whether to persist theme preference'),
|
||||||
|
}),
|
||||||
|
type: 'destructive',
|
||||||
|
},
|
||||||
|
|
||||||
|
handle: async (tab, params, response) => {
|
||||||
|
const { themeId, persist } = params;
|
||||||
|
|
||||||
|
if (!(themeId in builtInThemes)) {
|
||||||
|
response.addResult(`❌ Theme '${themeId}' not found. Available themes: ${Object.keys(builtInThemes).join(', ')}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const theme = builtInThemes[themeId]!;
|
||||||
|
const themeCode = `
|
||||||
|
// Apply MCP theme: ${theme.name}
|
||||||
|
if (window.mcpThemeManager) {
|
||||||
|
window.mcpThemeManager.setTheme('${themeId}');
|
||||||
|
} else {
|
||||||
|
// Apply theme variables directly
|
||||||
|
${Object.entries(theme.variables).map(([prop, value]) =>
|
||||||
|
`document.documentElement.style.setProperty('${prop}', '${value}');`
|
||||||
|
).join('\n ')}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Execute the theme change
|
||||||
|
await tab.waitForCompletion(async () => {
|
||||||
|
await (tab.page as any)._evaluateFunction(`() => { ${themeCode} }`);
|
||||||
|
});
|
||||||
|
|
||||||
|
response.addResult(`✅ Applied theme: **${theme.name}**`);
|
||||||
|
response.addResult(`Theme: ${theme.description}`);
|
||||||
|
if (persist) {
|
||||||
|
response.addResult(`💾 Theme preference saved`);
|
||||||
|
}
|
||||||
|
|
||||||
|
response.addCode(themeCode);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get current theme
|
||||||
|
const getTheme = defineTabTool({
|
||||||
|
capability: 'core',
|
||||||
|
schema: {
|
||||||
|
name: 'browser_mcp_theme_get',
|
||||||
|
title: 'Get current MCP theme',
|
||||||
|
description: 'Get details about the currently active MCP theme',
|
||||||
|
inputSchema: z.object({
|
||||||
|
includeVariables: z.boolean().optional().default(false).describe('Include CSS variables in response'),
|
||||||
|
}),
|
||||||
|
type: 'readOnly',
|
||||||
|
},
|
||||||
|
|
||||||
|
handle: async (tab, params, response) => {
|
||||||
|
const { includeVariables } = params;
|
||||||
|
|
||||||
|
// In a real implementation, this would check the current theme from the browser
|
||||||
|
const currentThemeId = 'minimal'; // Default theme
|
||||||
|
const theme = builtInThemes[currentThemeId]!;
|
||||||
|
|
||||||
|
if (!theme) {
|
||||||
|
response.addResult('❌ No theme currently active');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
response.addResult(`**Current Theme:** ${theme.name}`);
|
||||||
|
response.addResult(`**ID:** ${theme.id}`);
|
||||||
|
response.addResult(`**Description:** ${theme.description}`);
|
||||||
|
|
||||||
|
if (includeVariables) {
|
||||||
|
response.addResult(`\n**CSS Variables:**`);
|
||||||
|
Object.entries(theme.variables).forEach(([prop, value]) => {
|
||||||
|
response.addResult(`• ${prop}: ${value}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
response.addCode(`// Current MCP theme configuration`);
|
||||||
|
response.addCode(`const currentTheme = ${JSON.stringify(theme, null, 2)};`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create custom theme
|
||||||
|
const createTheme = defineTabTool({
|
||||||
|
capability: 'core',
|
||||||
|
schema: {
|
||||||
|
name: 'browser_mcp_theme_create',
|
||||||
|
title: 'Create custom MCP theme',
|
||||||
|
description: 'Create a new custom theme for MCP client identification',
|
||||||
|
inputSchema: z.object({
|
||||||
|
id: z.string().describe('Unique theme identifier'),
|
||||||
|
name: z.string().describe('Human-readable theme name'),
|
||||||
|
description: z.string().describe('Theme description'),
|
||||||
|
baseTheme: z.enum(['minimal', 'corporate', 'hacker', 'glass', 'highContrast']).optional().describe('Base theme to extend'),
|
||||||
|
variables: themeVariablesSchema.optional().describe('CSS custom properties to override'),
|
||||||
|
}),
|
||||||
|
type: 'destructive',
|
||||||
|
},
|
||||||
|
|
||||||
|
handle: async (tab, params, response) => {
|
||||||
|
const { id, name, description, baseTheme, variables } = params;
|
||||||
|
|
||||||
|
// Start with base theme or minimal default
|
||||||
|
const base = baseTheme ? builtInThemes[baseTheme]! : builtInThemes.minimal!;
|
||||||
|
|
||||||
|
const customTheme = {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
variables: {
|
||||||
|
...base.variables,
|
||||||
|
...variables
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
response.addResult(`✅ Created custom theme: **${name}**`);
|
||||||
|
response.addResult(`**ID:** ${id}`);
|
||||||
|
response.addResult(`**Description:** ${description}`);
|
||||||
|
if (baseTheme && baseTheme in builtInThemes) {
|
||||||
|
response.addResult(`**Based on:** ${builtInThemes[baseTheme]!.name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
response.addCode(`// Custom MCP theme: ${name}`);
|
||||||
|
response.addCode(`const customTheme = ${JSON.stringify(customTheme, null, 2)};`);
|
||||||
|
|
||||||
|
// Apply the new theme
|
||||||
|
const applyCode = `
|
||||||
|
// Apply custom theme
|
||||||
|
${Object.entries(customTheme.variables).map(([prop, value]) =>
|
||||||
|
`document.documentElement.style.setProperty('${prop}', '${value}');`
|
||||||
|
).join('\n')}
|
||||||
|
`;
|
||||||
|
|
||||||
|
await tab.waitForCompletion(async () => {
|
||||||
|
await (tab.page as any)._evaluateFunction(`() => { ${applyCode} }`);
|
||||||
|
});
|
||||||
|
response.addCode(applyCode);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset to default theme
|
||||||
|
const resetTheme = defineTabTool({
|
||||||
|
capability: 'core',
|
||||||
|
schema: {
|
||||||
|
name: 'browser_mcp_theme_reset',
|
||||||
|
title: 'Reset MCP theme',
|
||||||
|
description: 'Reset MCP client identification to default minimal theme',
|
||||||
|
inputSchema: z.object({
|
||||||
|
clearStorage: z.boolean().optional().default(true).describe('Clear stored theme preferences'),
|
||||||
|
}),
|
||||||
|
type: 'destructive',
|
||||||
|
},
|
||||||
|
|
||||||
|
handle: async (tab, params, response) => {
|
||||||
|
const { clearStorage } = params;
|
||||||
|
|
||||||
|
const defaultTheme = builtInThemes.minimal!;
|
||||||
|
|
||||||
|
const resetCode = `
|
||||||
|
// Reset MCP theme to default (minimal)
|
||||||
|
if (window.mcpThemeManager) {
|
||||||
|
window.mcpThemeManager.setTheme('minimal');
|
||||||
|
${clearStorage ? `localStorage.removeItem('mcp-theme');` : ''}
|
||||||
|
} else {
|
||||||
|
// Apply minimal theme variables directly
|
||||||
|
${Object.entries(defaultTheme.variables).map(([prop, value]) =>
|
||||||
|
`document.documentElement.style.setProperty('${prop}', '${value}');`
|
||||||
|
).join('\n ')}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
await tab.waitForCompletion(async () => {
|
||||||
|
await (tab.page as any)._evaluateFunction(`() => { ${resetCode} }`);
|
||||||
|
});
|
||||||
|
|
||||||
|
response.addResult(`✅ Reset to default theme: **${defaultTheme.name}**`);
|
||||||
|
response.addResult(`Theme: ${defaultTheme.description}`);
|
||||||
|
if (clearStorage) {
|
||||||
|
response.addResult(`🗑️ Cleared stored theme preferences`);
|
||||||
|
}
|
||||||
|
|
||||||
|
response.addCode(resetCode);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default [
|
||||||
|
listThemes,
|
||||||
|
setTheme,
|
||||||
|
getTheme,
|
||||||
|
createTheme,
|
||||||
|
resetTheme,
|
||||||
|
];
|
||||||
Loading…
x
Reference in New Issue
Block a user