Plugin Directory

Changeset 3464872

Timestamp:
02/19/2026 08:54:15 AM (5 weeks ago)
Author:
KingYes
Message:

Upload v1.1.0

Location:
angie/trunk
Files:
2 added
2 deleted
20 edited

Legend:

Unmodified
Added
Removed
  • angie/trunk/angie.php

    r3443255 r3464872  
    44 * Description: Agentic AI for WordPress
    55 * Plugin URI: https://elementor.com/pages/angie-early-access
    6  * Version: 1.0.4
     6 * Version: 1.1.0
    77 * Author: Elementor.com
    88 * Author URI: https://elementor.com/
     
    2121}
    2222
    23 define( 'ANGIE_VERSION', '1.0.4' );
     23define( 'ANGIE_VERSION', '1.1.0' );
    2424define( 'ANGIE_PATH', plugin_dir_path( __FILE__ ) );
    2525define( 'ANGIE_URL', plugins_url( '/', __FILE__ ) );
  • angie/trunk/modules/angie-app/components/angie-app.php

    r3443255 r3464872  
    55use Angie\Modules\ConsentManager\Module as ConsentManager;
    66use Angie\Modules\ConsentManager\Components\Consent_Page;
    7 use Angie\Modules\CodeSnippets\Module as CodeSnippets;
    87use Angie\Includes\Utils;
    98
     
    156155        wp_add_inline_script(
    157156            'angie-app',
    158             'window.angieConfig = ' . wp_json_encode( [
     157            'window.angieConfig = ' . wp_json_encode( apply_filters( 'angie_config', [
    159158                'plugins' => $plugins,
    160159                'installedPlugins' => $installed_plugins,
     
    165164                'untrusted__wpUserRole' => $wp_user_role, // Used only for analytics - Never use for auth decisions
    166165                'siteKey' => $this->get_site_key(),
    167                 'codeSnippetsActive' => CodeSnippets::is_active(),
    168166                'isElementorOneConnected' => $this->is_elementor_one_connected(),
    169             ] ),
     167            ] ) ),
    170168            'before'
    171169        );
     
    277275            <?php $this->render_app_styles(); ?>
    278276
    279         <script>
    280             (function() {
    281                 // Listen for messages from iframe about authentication status
    282                 window.addEventListener('message', function(event) {
    283                     // Check if message is from iframe about user being already authenticated
    284                     if (event.data && event.data.type === 'ANGIE_USER_ALREADY_AUTHENTICATED') {
    285                         console.log('User already authenticated');
    286                         // Open sidebar after a short delay
    287                         setTimeout(() => {
    288                             if (typeof window.toggleAngieSidebar === 'function') {
    289                                 window.toggleAngieSidebar(true);
    290                             }
    291                         }, 500);
    292                     }
    293                 });
    294 
    295                     <?php if ( $is_in_oauth_flow ) : ?>
    296                     const isStarting = <?php echo json_encode( $is_oauth_starting ); ?>;
    297                     const isReturning = <?php echo json_encode( $is_oauth_return ); ?>;
    298 
    299                     function ensureSidebarClosed() {
    300                         if (typeof window.toggleAngieSidebar === 'function') {
    301                             window.toggleAngieSidebar(false, true);
    302                         }
    303                     }
    304 
    305                     function isOAuthComplete() {
    306                         const urlParams = new URLSearchParams(window.location.search);
    307                         return !urlParams.has('oauth_code') && !urlParams.has('oauth_state') && !urlParams.has('start-oauth');
    308                     }
    309 
    310                     function updateUIAfterAuth() {
    311                         const appStart = document.getElementById('angie-app-start');
    312                         if (appStart) {
    313                             // Remove loading state if it exists
    314                             const loadingState = appStart.querySelector('.angie-loading-state');
    315                             if (loadingState) {
    316                                 loadingState.remove();
    317                             }
    318                         }
    319                     }
    320 
    321                     function openSidebarAfterAuth() {
    322                         try {
    323                             localStorage.setItem('angie_sidebar_state', 'open');
    324                         } catch (e) {}
    325                         if (typeof window.toggleAngieSidebar === 'function') {
    326                             setTimeout(() => window.toggleAngieSidebar(true), 500);
    327                         }
    328                     }
    329 
    330                     function monitorAuthCompletion() {
    331                         let authenticationSuccessful = false;
    332                         const checkInterval = setInterval(function() {
    333                             const sidebar = document.getElementById('angie-sidebar-container');
    334                             if (sidebar?.querySelector('iframe') && isOAuthComplete()) {
    335                                 updateUIAfterAuth();
    336                                 clearInterval(checkInterval);
    337                                 authenticationSuccessful = true;
    338                                 openSidebarAfterAuth();
    339                                 window.location.reload(); // Reload to update MCPs
    340                             }
    341                         }, 500);
    342 
    343                         setTimeout(function() {
    344                             clearInterval(checkInterval);
    345                             if (!authenticationSuccessful) {
    346                                 console.log('OAuth authentication timed out');
    347                                 ensureSidebarClosed();
    348                             }
    349                         }, 30000);
    350                     }
    351 
    352                     ensureSidebarClosed();
    353 
    354                     if (isStarting && !isReturning) {
    355                         console.log('OAuth flow starting, waiting for redirect...');
    356                     } else if (isReturning) {
    357                         monitorAuthCompletion();
    358                     }
    359                     <?php endif; ?>
    360                 })();
    361             </script>
    362277        <?php else : ?>
    363278            <div class="wrap">
  • angie/trunk/modules/code-snippets/assets/css/list-table-toggle.css

    r3443255 r3464872  
     1.wrap .page-title-action {
     2    display: none;
     3}
     4
     5.angie-snippet-tooltip-trigger {
     6    display: inline-flex;
     7    align-items: center;
     8    justify-content: center;
     9    cursor: help;
     10}
     11
     12.angie-snippet-info-icon {
     13    display: inline-flex;
     14    align-items: center;
     15    justify-content: center;
     16    width: 16px;
     17    height: 16px;
     18    border-radius: 50%;
     19    background-color: #72777c;
     20    color: #fff;
     21    font-size: 11px;
     22    font-weight: 600;
     23    font-style: italic;
     24    line-height: 1;
     25}
     26
     27.angie-snippet-tooltip-trigger:hover .angie-snippet-info-icon {
     28    background-color: #2271b1;
     29}
     30
     31.angie-snippet-tooltip-popover {
     32    position: fixed;
     33    z-index: 100000;
     34    display: none;
     35    max-width: 260px;
     36    padding: 10px 12px;
     37    background-color: #1d2327;
     38    color: #f0f0f1;
     39    font-size: 13px;
     40    line-height: 1.4;
     41    border-radius: 4px;
     42    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.3);
     43    pointer-events: none;
     44}
     45
     46.angie-snippet-tooltip-popover::after {
     47    content: "";
     48    position: absolute;
     49    top: 100%;
     50    left: 50%;
     51    margin-left: -6px;
     52    border: 6px solid transparent;
     53    border-top-color: #1d2327;
     54}
     55
    156.angie-snippet-toggle {
    257    position: relative;
     
    72127    display: inline-flex;
    73128    align-items: center;
    74     gap: 8px;
     129    gap: 6px;
     130}
     131
     132.angie-env-synced {
     133    color: #00a32a;
     134}
     135
     136.angie-env-not-synced {
     137    color: #dba617;
     138}
     139
     140.angie-env-not-deployed,
     141.angie-env-test-only {
     142    color: #d63638;
    75143}
    76144
  • angie/trunk/modules/code-snippets/classes/assets-manager.php

    r3443255 r3464872  
    1212    public static function init() {
    1313        add_action( 'admin_enqueue_scripts', [ __CLASS__, 'enqueue_code_editor_assets' ] );
    14         add_action( 'admin_enqueue_scripts', [ __CLASS__, 'enqueue_dev_mode_assets' ] );
    15     }
    16 
    17     public static function enqueue_dev_mode_assets() {
    18         $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
    19         if ( ! $screen || 'edit-' . Module::CPT_NAME !== $screen->id ) {
    20             return;
    21         }
    22 
    23         if ( ! Module::current_user_can_manage_snippets() ) {
    24             return;
    25         }
    26 
    27         $script_url = plugins_url( 'assets/js/dev-mode.js', dirname( __FILE__ ) );
    28         wp_enqueue_script(
    29             'angie-dev-mode',
    30             $script_url,
    31             [ 'jquery' ],
    32             '1.0.0',
    33             true
    34         );
    35 
    36         wp_localize_script(
    37             'angie-dev-mode',
    38             'angieDevMode',
    39             [
    40                 'restUrl' => esc_url( rest_url( 'angie/v1/dev-mode' ) ),
    41                 'nonce'   => wp_create_nonce( 'wp_rest' ),
    42             ]
    43         );
    4414    }
    4515
  • angie/trunk/modules/code-snippets/classes/deployment-meta-box.php

    r3443255 r3464872  
    1414        add_action( 'save_post_' . Module::CPT_NAME, [ __CLASS__, 'save_deployment_meta' ], 5 );
    1515        add_action( 'admin_post_angie_delete_environment', [ __CLASS__, 'handle_delete_environment' ] );
     16        add_action( 'admin_enqueue_scripts', [ __CLASS__, 'enqueue_assets' ] );
     17        add_action( 'post_submitbox_misc_actions', [ __CLASS__, 'render_publish_box_toggle' ] );
     18        add_filter( 'angie_config', [ __CLASS__, 'add_config' ] );
     19    }
     20
     21    public static function enqueue_assets( $hook ) {
     22        if ( 'post.php' !== $hook && 'post-new.php' !== $hook ) {
     23            return;
     24        }
     25
     26        $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
     27        if ( ! $screen || Module::CPT_NAME !== $screen->post_type ) {
     28            return;
     29        }
     30
     31        wp_enqueue_style(
     32            'angie-list-table-toggle',
     33            plugins_url( 'assets/css/list-table-toggle.css', dirname( __FILE__ ) ),
     34            [],
     35            ANGIE_VERSION
     36        );
     37
     38        wp_add_inline_style(
     39            'angie-list-table-toggle',
     40            '#misc-publishing-actions .misc-pub-section:not(.angie-publish-toggle) { display: none !important; }'
     41        );
     42    }
     43
     44    public static function add_config( $config ) {
     45        $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
     46        if ( ! $screen || Module::CPT_NAME !== $screen->post_type ) {
     47            return $config;
     48        }
     49
     50        if ( 'post' !== $screen->base && 'post-new' !== $screen->base ) {
     51            return $config;
     52        }
     53
     54        $config['deploymentMetaBox'] = [
     55            'ajaxUrl' => admin_url( 'admin-ajax.php' ),
     56            'nonce'   => wp_create_nonce( 'angie_toggle_snippet_status' ),
     57        ];
     58
     59        return $config;
     60    }
     61
     62    public static function render_publish_box_toggle( $post ) {
     63        if ( Module::CPT_NAME !== get_post_type( $post ) ) {
     64            return;
     65        }
     66
     67        $is_published = 'publish' === $post->post_status;
     68        $checked      = $is_published ? 'checked' : '';
     69
     70        echo '<div class="misc-pub-section angie-publish-toggle" style="display: flex !important; align-items: center; gap: 8px; padding: 6px 10px;">';
     71        echo '<strong>' . esc_html__( 'Status:', 'angie' ) . '</strong>';
     72        printf(
     73            '<label class="angie-snippet-toggle">
     74                <input type="checkbox" class="angie-snippet-toggle-input" data-post-id="%d" %s />
     75                <span class="angie-snippet-toggle-slider"></span>
     76            </label>',
     77            absint( $post->ID ),
     78            esc_attr( $checked )
     79        );
     80        echo '<span class="angie-metabox-status-label">' . ( $is_published ? esc_html__( 'Active', 'angie' ) : esc_html__( 'Inactive', 'angie' ) ) . '</span>';
     81        echo '</div>';
    1682    }
    1783
     
    3399        $dev_time = $timestamps['dev'];
    34100        $prod_time = $timestamps['prod'];
    35         $sync_status = $timestamps['status'];
    36101        $delete_url_base = admin_url( 'admin-post.php' );
    37102
     
    78143
    79144        echo '<p><strong>' . esc_html__( 'Sync Status:', 'angie' ) . '</strong><br>';
    80         if ( Dev_Mode_Manager::SYNC_STATUS_NOT_DEPLOYED === $sync_status ) {
    81            echo '<span style="color: #999;">' . esc_html__( 'Not Deployed', 'angie' ) . '</span>';
    82         } elseif ( Dev_Mode_Manager::SYNC_STATUS_CHANGES_PENDING === $sync_status ) {
    83            echo '<span style="color: #d63638;">' . esc_html__( 'Test & Live not synced', 'angie' ) . '</span>';
    84         } elseif ( Dev_Mode_Manager::SYNC_STATUS_TEST_ONLY === $sync_status ) {
    85            echo '<span style="color: #d63638;">' . esc_html__( 'Test Environment only', 'angie' ) . '</span>';
    86         } else {
    87            echo '<span style="color: #00a32a;">' . esc_html__( 'Live & Synced', 'angie' ) . '</span>';
    88         }
     145        List_Table_Manager::render_sync_status( $post->ID );
    89146        echo '</p>';
    90147
    91148        echo '<p>';
    92         $button_text = ( $dev_time > 0 ) ? esc_html__( 'Push to Production', 'angie' ) : esc_html__( 'Publish to Dev', 'angie' );
    93         submit_button( $button_text, 'primary', 'angie_push_to_production', false );
     149        $deploy_action = ( $dev_time > 0 ) ? 'push-to-production' : 'publish-to-dev';
     150        $button_text = ( $dev_time > 0 ) ? esc_html__( 'Push to Production', 'angie' ) : esc_html__( 'Push to Test', 'angie' );
     151        submit_button( $button_text, 'primary', 'angie_push_to_production', false, [ 'data-action' => $deploy_action ] );
    94152        echo '</p>';
    95153        echo '</div>';
  • angie/trunk/modules/code-snippets/classes/dev-mode-admin-ui.php

    r3443255 r3464872  
    1212    public static function init() {
    1313        add_action( 'admin_notices', [ __CLASS__, 'render_dev_mode_notice' ] );
    14         add_action( 'admin_footer', [ __CLASS__, 'render_dev_mode_exit_button' ] );
    15 
    16         add_action( 'elementor/editor/after_enqueue_scripts', [ __CLASS__, 'enqueue_elementor_dev_mode_assets' ] );
    17         add_action( 'elementor/editor/footer', [ __CLASS__, 'render_elementor_dev_mode_exit_button' ] );
     14        add_filter( 'angie_config', [ __CLASS__, 'add_dev_mode_state_to_angie_config' ] );
    1815    }
    1916
     
    3330            echo '<div class="notice notice-warning is-dismissible">';
    3431            echo '<p>';
    35             echo '<strong>' . esc_html__( 'Dev Mode is Active', 'angie' ) . '</strong> - ' . esc_html__( 'Snippets are loading from the development environment.', 'angie' ) . ' ';
    36             echo '<button type="button" class="button button-secondary" id="angie-disable-dev-mode">' . esc_html__( 'Disable Dev Mode', 'angie' ) . '</button>';
     32            echo '<strong>' . esc_html__( 'Test Mode is Active', 'angie' ) . '</strong> - ' . esc_html__( 'Snippets are loading from the development environment.', 'angie' ) . ' ';
     33            echo '<button type="button" class="button button-secondary" id="angie-disable-dev-mode">' . esc_html__( 'Disable Test Mode', 'angie' ) . '</button>';
    3734            echo '</p>';
    3835            echo '</div>';
     
    4138            echo '<p>';
    4239            echo esc_html__( 'Snippets are loading from production.', 'angie' ) . ' ';
    43             echo '<button type="button" class="button button-primary" id="angie-enable-dev-mode">' . esc_html__( 'Enable Dev Mode', 'angie' ) . '</button>';
     40            echo '<button type="button" class="button button-primary" id="angie-enable-dev-mode">' . esc_html__( 'Enable Test Mode', 'angie' ) . '</button>';
    4441            echo '</p>';
    4542            echo '</div>';
     
    4744    }
    4845
    49     public static function render_dev_mode_exit_button() {
    50         if ( ! is_admin() ) {
    51             return;
    52         }
    53 
    54         if ( ! Module::current_user_can_manage_snippets() ) {
    55             return;
    56         }
    57 
    58         if ( ! Dev_Mode_Manager::is_dev_mode_enabled() ) {
    59             return;
    60         }
    61 
    62         self::enqueue_dev_mode_border_assets();
    63         self::render_angie_dev_mode_exit_button_html();
    64     }
    65 
    66     private static function enqueue_dev_mode_border_assets() {
    67         wp_enqueue_style(
    68             'angie-dev-mode-border',
    69             plugins_url( 'assets/css/dev-mode-border.css', dirname( __FILE__ ) ),
    70             [],
    71             ANGIE_VERSION
    72         );
    73 
    74         wp_enqueue_script(
    75             'angie-dev-mode-exit-button',
    76             plugins_url( 'assets/js/dev-mode-exit-button.js', dirname( __FILE__ ) ),
    77             [],
    78             ANGIE_VERSION,
    79             true
    80         );
    81 
    82         wp_localize_script(
    83             'angie-dev-mode-exit-button',
    84             'angieDevModeExit',
    85             [
    86                 'restUrl' => esc_url( rest_url( 'angie/v1/dev-mode' ) ),
    87                 'nonce' => wp_create_nonce( 'wp_rest' ),
    88                 'buttonText' => esc_html__( 'Test mode', 'angie' ),
    89                 'exitingText' => esc_html__( 'Exiting...', 'angie' ),
    90                 'errorText' => esc_html__( 'An error occurred.', 'angie' ),
    91                 'requestFailedText' => esc_html__( 'Request failed.', 'angie' ),
    92             ]
    93         );
    94     }
    95 
    96     private static function render_angie_dev_mode_exit_button_html() {
    97         ?>
    98         <div id="angie-dev-mode-border-overlay"></div>
    99         <div id="angie-dev-mode-button-wrapper">
    100             <div id="angie-dev-mode-tooltip" class="angie-tooltip-hidden">
    101                 <p class="angie-tooltip-title"><?php echo esc_html__( 'Test mode lets you try things safely.', 'angie' ); ?></p>
    102                 <p><?php echo esc_html__( 'Changes you make here are saved as drafts and won\'t affect your live site until you publish them.', 'angie' ); ?></p>
    103                 <p><?php echo esc_html__( 'You can create, edit, and preview widgets freely, then publish when you\'re ready.', 'angie' ); ?></p>
    104                 <div class="angie-tooltip-arrow"></div>
    105             </div>
    106             <button type="button" id="angie-dev-mode-exit-button">
    107                 <span class="angie-exit-icon">✕</span>
    108                 <span><?php echo esc_html__( 'Test mode', 'angie' ); ?></span>
    109                 <span class="angie-info-icon">i</span>
    110             </button>
    111         </div>
    112         <?php
    113     }
    114 
    115     public static function enqueue_elementor_dev_mode_assets() {
    116         if ( ! Module::current_user_can_manage_snippets() ) {
    117             return;
    118         }
    119 
    120         if ( ! Dev_Mode_Manager::is_dev_mode_enabled() ) {
    121             return;
    122         }
    123 
    124         self::enqueue_dev_mode_border_assets();
    125     }
    126 
    127     public static function render_elementor_dev_mode_exit_button() {
    128         if ( ! Module::current_user_can_manage_snippets() ) {
    129             return;
    130         }
    131 
    132         if ( ! Dev_Mode_Manager::is_dev_mode_enabled() ) {
    133             return;
    134         }
    135 
    136         self::render_angie_dev_mode_exit_button_html();
     46    public static function add_dev_mode_state_to_angie_config( $angie_config ) {
     47        $angie_config['isDevModeEnabled'] = Dev_Mode_Manager::is_dev_mode_enabled();
     48        return $angie_config;
    13749    }
    13850}
  • angie/trunk/modules/code-snippets/classes/dev-mode-manager.php

    r3443255 r3464872  
    4242    }
    4343
    44     public static function create_dev_mode_session() {
    45         $user_id = get_current_user_id();
    46         if ( ! $user_id ) {
    47             return false;
    48         }
    49 
    50         $ip_address = self::get_client_ip();
    51         if ( empty( $ip_address ) ) {
    52             return false;
    53         }
    54 
     44    private static function set_dev_mode_cookie( $user_id, $ip_address ) {
    5545        $token = self::generate_session_token( $user_id, $ip_address );
    56         $expiry = time() + HOUR_IN_SECONDS;
     46        $expiry = time() + YEAR_IN_SECONDS;
    5747        $ip_hash = hash( 'sha256', $ip_address );
    5848        $cookie_value = $token . '|' . $expiry . '|' . $user_id . '|' . $ip_hash;
     
    6757            true
    6858        );
     59
     60        return $expiry;
     61    }
     62
     63    private static function extend_dev_mode_session( $user_id, $ip_address ) {
     64        if ( headers_sent() ) {
     65            return;
     66        }
     67
     68        self::set_dev_mode_cookie( $user_id, $ip_address );
     69    }
     70
     71    public static function create_dev_mode_session() {
     72        $user_id = get_current_user_id();
     73        if ( ! $user_id ) {
     74            return false;
     75        }
     76
     77        $ip_address = self::get_client_ip();
     78        if ( empty( $ip_address ) ) {
     79            return false;
     80        }
     81
     82        $expiry = self::set_dev_mode_cookie( $user_id, $ip_address );
    6983
    7084        return [
     
    120134        }
    121135
     136        self::extend_dev_mode_session( $user_id, $current_ip );
     137
    122138        return true;
    123139    }
  • angie/trunk/modules/code-snippets/classes/fatal-error-handler.php

    r3443255 r3464872  
    88class Fatal_Error_Handler {
    99
    10     const FATAL_ERROR_TYPES = [ E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR ];
     10    private const ERROR_CODE_ADDITIONAL_MESSAGE = 'angie_additional_message';
    1111
    1212    public static function init() {
     
    1515        }
    1616
     17        add_filter( 'wp_die_handler', function() {
     18            return [ __CLASS__, 'handle_wp_die' ];
     19        }, 9999 );
     20    }
     21
     22    private static function get_additional_error_message( $message ): string {
     23
     24        $error_message = self::format_error_message( $message );
     25        $exit_url = self::get_exit_test_mode_url();
     26        $snippet_id = self::extract_snippet_id( $error_message );
     27        $prompt = $snippet_id
     28            ? 'Please fix the error in snippet ID ' . $snippet_id . ' - Use the snippet slug: ' . $error_message
     29            : 'Please fix this error: ' . $error_message;
     30        $fix_with_angie_url = $exit_url . '#angie-prompt=' . rawurlencode( $prompt );
     31
    1732        ob_start();
    18         register_shutdown_function( [ __CLASS__, 'handle_shutdown' ] );
     33        ?>
     34        <style>
     35            body#error-page {
     36                border: none !important;
     37                max-width: none !important;
     38                margin: 0 !important;
     39                padding: 0 !important;
     40                font-size: 0 !important;
     41                height: 100vh !important;
     42                display: flex !important;
     43                justify-content: center !important;
     44                align-items: center !important;
     45            }
     46            body#error-page .wp-die-message ul {
     47                margin: 0 !important;
     48                padding: 0 !important;
     49            }
     50            body#error-page .wp-die-message ul > li {
     51                display: none !important;
     52            }
     53            body#error-page .wp-die-message ul > li:first-child {
     54                display: block !important;
     55            }
     56
     57            .angie-fatal-notice {
     58                width: 550px;
     59                max-width: 90vw;
     60                padding: 24px;
     61                border-radius: 16px;
     62                font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
     63                color: #1d2327;
     64                border: 1px solid #e0e0e0;
     65            }
     66            .angie-fatal-notice h2 {
     67                font-size: 20px;
     68                margin: 0 0 12px 0;
     69            }
     70            #error-page .angie-fatal-notice p {
     71                font-size: 16px;
     72                color: #50575e;
     73                margin: 0 0 4px 0;
     74            }
     75            .angie-fatal-notice .angie-help-link {
     76                display: inline-block;
     77                font-size: 16px;
     78                color: #2271b1;
     79                margin-top: 12px;
     80                margin-bottom: 24px;
     81            }
     82            .angie-fatal-notice .angie-help-link:hover {
     83                color: #135e96;
     84            }
     85            .angie-fatal-buttons {
     86                display: flex;
     87                justify-content: flex-end;
     88                align-items: center;
     89                gap: 16px;
     90            }
     91            .angie-btn {
     92                display: inline-block;
     93                padding: 8px 16px;
     94                font-size: 14px;
     95                font-weight: 500;
     96                text-decoration: none;
     97                border-radius: 10px;
     98                cursor: pointer;
     99                text-align: center;
     100                background: transparent;
     101                color: #1d2327;
     102                border: none;
     103                transition: all 0.2s ease;
     104            }
     105            .angie-btn:hover {
     106                background: #f0f0f1;
     107            }
     108            .angie-fix-with-angie-btn {
     109                background: #1d2327;
     110                color: #fff;
     111                border: 1px solid #1d2327;
     112            }
     113            .angie-fix-with-angie-btn:hover {
     114                background: #3a3f44;
     115                color: #fff;
     116                border-color: #3a3f44;
     117            }
     118        </style>
     119        <div class="angie-fatal-notice">
     120            <h2><?php echo esc_html__( 'Something went wrong while working on your site', 'angie' ); ?></h2>
     121            <p><?php echo esc_html__( 'A change made by Angie caused an error in this preview.', 'angie' ); ?></p>
     122            <p><?php echo esc_html__( "Your live site is not affected - you're viewing changes in Test Mode.", 'angie' ); ?></p>
     123            <a href="https://wordpress.org/documentation/article/faq-troubleshooting/" target="_blank" rel="noopener noreferrer" class="angie-help-link">
     124                <?php echo esc_html__( 'Learn how to troubleshoot WordPress errors', 'angie' ); ?>
     125            </a>
     126            <div class="angie-fatal-buttons">
     127                <a href="<?php echo esc_url( $exit_url ); ?>" class="angie-btn">
     128                    <?php echo esc_html__( 'Exit Test Mode', 'angie' ); ?>
     129                </a>
     130                <a href="<?php echo esc_url( $fix_with_angie_url ); ?>" class="angie-btn angie-fix-with-angie-btn">
     131                    <?php echo esc_html__( 'Fix with Angie', 'angie' ); ?>
     132                </a>
     133            </div>
     134        </div>
     135        <?php
     136        return ob_get_clean();
     137    }
     138
     139    public static function handle_wp_die( $message, $title = '', $args = [] ): void {
     140        $additional_message = static::get_additional_error_message( $message );
     141
     142        $message = is_wp_error( $message )
     143            ? self::merge_wp_errors( $message, $additional_message )
     144            : $additional_message . $message;
     145
     146        _default_wp_die_handler( $message, $title, $args );
     147    }
     148
     149    private static function merge_wp_errors( \WP_Error $original, string $additional_message ): \WP_Error {
     150        $new_error = new \WP_Error( self::ERROR_CODE_ADDITIONAL_MESSAGE, $additional_message );
     151
     152        foreach ( $original->get_error_codes() as $code ) {
     153            foreach ( $original->get_error_messages( $code ) as $error_message ) {
     154                $new_error->add( $code, $error_message );
     155            }
     156
     157            $error_data = $original->get_error_data( $code );
     158
     159            if ( $error_data ) {
     160                $new_error->add_data( $error_data, $code );
     161            }
     162        }
     163
     164        return $new_error;
    19165    }
    20166
     
    37183
    38184        return Dev_Mode_Manager::is_dev_mode_enabled();
    39     }
    40 
    41     public static function handle_shutdown(): void {
    42         $error = error_get_last();
    43 
    44         if ( ! $error || ! in_array( $error['type'], self::FATAL_ERROR_TYPES, true ) ) {
    45             return;
    46         }
    47 
    48         if ( ! Dev_Mode_Manager::is_dev_mode_enabled() ) {
    49             return;
    50         }
    51 
    52         while ( ob_get_level() > 0 ) {
    53             ob_end_clean();
    54         }
    55 
    56         self::render_fallback_notice();
    57     }
    58 
    59     private static function render_fallback_notice(): void {
    60         $exit_url = self::get_exit_test_mode_url();
    61         ?>
    62 <!DOCTYPE html>
    63 <html>
    64 <head>
    65     <meta charset="utf-8">
    66     <meta name="viewport" content="width=device-width, initial-scale=1">
    67     <title><?php echo esc_html__( 'Error in Test Mode', 'angie' ); ?></title>
    68     <style>
    69         .angie-fatal-notice {
    70             background: #fff;
    71             border-left: 4px solid #ED01EE;
    72             box-shadow: 0 1px 3px rgba(0,0,0,0.1);
    73             max-width: 600px;
    74             padding: 14px 24px;
    75             border-radius: 4px;
    76             display: block;
    77         }
    78         .angie-fatal-notice h2 {
    79             display: block;
    80             font-size: 18px;
    81             font-weight: 600;
    82             color: #1d2327;
    83             margin-bottom: 12px;
    84         }
    85         .angie-fatal-notice p {
    86             display: block;
    87             font-size: 14px;
    88             line-height: 1.6;
    89             color: #50575e;
    90             margin-bottom: 20px;
    91         }
    92         .angie-fatal-buttons {
    93             display: block;
    94         }
    95         .angie-btn {
    96             display: inline-block;
    97             padding: 10px 20px;
    98             font-size: 14px;
    99             font-weight: 500;
    100             text-decoration: none;
    101             border-radius: 4px;
    102             cursor: pointer;
    103             text-align: center;
    104             background: #f0f0f1;
    105             color: #1d2327;
    106             border: 1px solid #c3c4c7;
    107         }
    108     </style>
    109 </head>
    110 <body>
    111     <div class="angie-fatal-notice">
    112         <h2><?php echo esc_html__( 'Something went wrong', 'angie' ); ?></h2>
    113         <p><?php echo esc_html__( "Don't worry, the website's visitors won't see this because you are previewing unpublished changes in test mode.", 'angie' ); ?></p>
    114         <div class="angie-fatal-buttons">
    115             <a href="<?php echo esc_url( $exit_url ); ?>" class="angie-btn">
    116                 <?php echo esc_html__( 'Exit test mode', 'angie' ); ?>
    117             </a>
    118         </div>
    119     </div>
    120 </body>
    121 </html>
    122         <?php
    123         exit;
    124185    }
    125186
     
    128189        return add_query_arg( 'angie-exit-test-mode', '1', $current_url );
    129190    }
     191
     192    private static function extract_snippet_id( string $error_message ): ?string {
     193        if ( preg_match( '/snippet-(\d+)/', $error_message, $matches ) ) {
     194            return $matches[1];
     195        }
     196        return null;
     197    }
     198
     199    private static function format_error_message( $message ): string {
     200
     201        if ( is_wp_error( $message ) ) {
     202            if( isset( $message->error_data['internal_server_error']['error']['message'] ) ) {
     203                return $message->error_data['internal_server_error']['error']['message'];
     204            }
     205            return $message->get_error_message();
     206        }
     207
     208        if ( is_string( $message ) ) {
     209            return $message;
     210        }
     211
     212        return wp_json_encode( $message ) ?? '';
     213    }
    130214}
  • angie/trunk/modules/code-snippets/classes/file-system-handler.php

    r3443255 r3464872  
    6464            $file_path = $base_dir . '/' . $filename;
    6565            $wp_filesystem->put_contents( $file_path, $content, FS_CHMOD_FILE );
     66
     67            self::maybe_invalidate_opcache( $file_path );
    6668        }
     69    }
     70
     71    private static function maybe_invalidate_opcache( $filepath ) {
     72        if ( ! function_exists( 'wp_opcache_invalidate' ) ) {
     73            return;
     74        }
     75
     76        wp_opcache_invalidate( $filepath, true );
    6777    }
    6878
  • angie/trunk/modules/code-snippets/classes/files-meta-box.php

    r3443255 r3464872  
    3434
    3535        wp_nonce_field( 'angie_snippet_files_save', 'angie_snippet_files_nonce' );
    36 
    37         self::enqueue_files_meta_box_script();
    3836
    3937        echo '<div id="angie-snippet-files">';
     
    7876    }
    7977
    80     private static function enqueue_files_meta_box_script() {
    81         $script_url = plugins_url( 'assets/js/files-meta-box.js', dirname( __FILE__ ) );
    82         wp_enqueue_script(
    83             'angie-files-meta-box',
    84             $script_url,
    85             [ 'code-editor' ],
    86             '1.0.0',
    87             true
    88         );
    89 
    90         wp_localize_script(
    91             'angie-files-meta-box',
    92             'angieFilesMetaBox',
    93             [
    94                 'placeholders' => [
    95                     'name'    => esc_attr__( 'e.g. main.php', 'angie' ),
    96                     'content' => esc_attr__( 'File content…', 'angie' ),
    97                 ],
    98                 'labels'       => [
    99                     'remove' => esc_html__( 'Remove', 'angie' ),
    100                 ],
    101             ]
    102         );
    103     }
    104 
    10578    public static function save_files_meta( $post_id ) {
    10679        if ( ! isset( $_POST['angie_snippet_files_nonce'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
     
    131104
    132105        foreach ( $raw_files as $file ) {
    133             $name = isset( $file['name'] ) ? sanitize_text_field( wp_unslash( $file['name'] ) ) : '';
    134             // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Unslashing to remove WP magic quotes; base64 encoded immediately.
    135             $content_clean = isset( $file['content'] ) ? wp_unslash( $file['content'] ) : '';
     106            $name = isset( $file['name'] ) ? sanitize_text_field( $file['name'] ) : '';
     107            // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Already unslashed via $raw_files; base64 encoded immediately.
     108            $content_clean = isset( $file['content'] ) ? $file['content'] : '';
    136109
    137110            if ( '' === trim( $name ) && '' === trim( $content_clean ) ) {
  • angie/trunk/modules/code-snippets/classes/list-table-manager.php

    r3443255 r3464872  
    1414        add_action( 'manage_' . Module::CPT_NAME . '_posts_custom_column', [ __CLASS__, 'render_custom_columns' ], 10, 2 );
    1515        add_action( 'admin_enqueue_scripts', [ __CLASS__, 'enqueue_assets' ] );
     16        add_filter( 'angie_config', [ __CLASS__, 'add_config' ] );
    1617        add_action( 'wp_ajax_angie_toggle_snippet_status', [ __CLASS__, 'ajax_toggle_status' ] );
    1718        add_action( 'wp_ajax_angie_push_to_production', [ __CLASS__, 'ajax_push_to_production' ] );
     19        add_filter( 'post_row_actions', [ __CLASS__, 'remove_quick_edit' ], 10, 2 );
     20    }
     21
     22    public static function remove_quick_edit( $actions, $post ) {
     23        if ( Module::CPT_NAME === $post->post_type ) {
     24            unset( $actions['inline hide-if-no-js'] );
     25        }
     26
     27        return $actions;
    1828    }
    1929
     
    2636            'files' => esc_html__( 'Files', 'angie' ),
    2737            'actions' => esc_html__( 'Actions', 'angie' ),
    28             'date' => $columns['date'],
     38            'last_modified' => esc_html__( 'Last Modified', 'angie' ),
    2939        ];
    3040
     
    5363        } elseif ( 'actions' === $column ) {
    5464            self::render_actions_column( $post_id );
     65        } elseif ( 'last_modified' === $column ) {
     66            self::render_last_modified_column( $post_id );
    5567        }
    5668    }
     
    7587    }
    7688
     89    private static function render_last_modified_column( $post_id ) {
     90        $post = get_post( $post_id );
     91        if ( ! $post ) {
     92            return;
     93        }
     94
     95        if ( '0000-00-00 00:00:00' === $post->post_modified ) {
     96            $t_time = esc_html__( 'Unpublished', 'angie' );
     97        } else {
     98            $t_time = sprintf(
     99                /* translators: 1: Post date, 2: Post time. */
     100                esc_html__( '%1$s at %2$s', 'angie' ),
     101                /* translators: Post date format. See https://www.php.net/manual/datetime.format.php */
     102                get_the_modified_time( esc_html__( 'Y/m/d', 'angie' ), $post ),
     103                /* translators: Post time format. See https://www.php.net/manual/datetime.format.php */
     104                get_the_modified_time( esc_html__( 'g:i a', 'angie' ), $post )
     105            );
     106        }
     107
     108        echo esc_html( $t_time );
     109    }
     110
    77111    private static function render_files_column( $post_id ) {
    78112        $files = get_post_meta( $post_id, '_angie_snippet_files', true );
     
    85119
    86120    private static function render_environment_column( $post_id ) {
     121        self::render_sync_status( $post_id );
     122    }
     123
     124    public static function render_sync_status( $post_id ) {
    87125        $timestamps = Dev_Mode_Manager::get_snippet_environment_timestamps( $post_id );
    88126        $sync_status = $timestamps['status'];
    89127
    90128        if ( Dev_Mode_Manager::SYNC_STATUS_NOT_DEPLOYED === $sync_status ) {
    91            echo '<span class="angie-env-status angie-env-not-deployed">';
    92            echo '<span class="angie-env-badge angie-env-badge-gray"></span>';
    93            echo esc_html__( 'Not Deployed', 'angie' );
    94            echo '</span>';
     129            $tooltip = esc_attr__( 'Snippet has not been deployed to test or live yet', 'angie' );
     130            echo '<span class="angie-env-status angie-env-not-deployed">';
     131            echo '<span class="angie-env-badge angie-env-badge-test"></span>';
     132            echo esc_html__( 'Not Deployed', 'angie' );
     133            self::render_tooltip_icon( $tooltip );
     134            echo '</span>';
    95135        } elseif ( Dev_Mode_Manager::SYNC_STATUS_CHANGES_PENDING === $sync_status ) {
    96            echo '<span class="angie-env-status angie-env-not-synced">';
    97            echo '<span class="angie-env-badge angie-env-badge-warning"></span>';
    98            echo esc_html__( 'Test & Live not synced', 'angie' );
    99            echo '</span>';
     136            $tooltip = esc_attr__( 'Snippet is live but has unpublished changes in test', 'angie' );
     137            echo '<span class="angie-env-status angie-env-not-synced">';
     138            echo '<span class="angie-env-badge angie-env-badge-warning"></span>';
     139            echo esc_html__( 'Live (out of sync)', 'angie' );
     140            self::render_tooltip_icon( $tooltip );
     141            echo '</span>';
    100142        } elseif ( Dev_Mode_Manager::SYNC_STATUS_TEST_ONLY === $sync_status ) {
    101            echo '<span class="angie-env-status angie-env-test-only">';
    102            echo '<span class="angie-env-badge angie-env-badge-test"></span>';
    103            echo esc_html__( 'Test Environment only', 'angie' );
    104            echo '</span>';
     143            $tooltip = esc_attr__( 'Snippet is only deployed to sandbox/test, not to live', 'angie' );
     144            echo '<span class="angie-env-status angie-env-test-only">';
     145            echo '<span class="angie-env-badge angie-env-badge-test"></span>';
     146            echo esc_html__( 'Sandbox only', 'angie' );
     147            self::render_tooltip_icon( $tooltip );
     148            echo '</span>';
    105149        } else {
    106            echo '<span class="angie-env-status angie-env-synced">';
    107            echo '<span class="angie-env-badge angie-env-badge-success"></span>';
    108            echo esc_html__( 'Live & Synced', 'angie' );
    109            echo '</span>';
    110         }
     150            $tooltip = esc_attr__( 'Snippet is deployed to live and matches the latest version', 'angie' );
     151            echo '<span class="angie-env-status angie-env-synced">';
     152            echo '<span class="angie-env-badge angie-env-badge-success"></span>';
     153            echo esc_html__( 'Live (synced)', 'angie' );
     154            self::render_tooltip_icon( $tooltip );
     155            echo '</span>';
     156        }
     157    }
     158
     159    private static function render_tooltip_icon( $tooltip_text ) {
     160        printf(
     161            '<span class="angie-snippet-tooltip-trigger" data-tooltip="%s" aria-label="%s">',
     162            esc_attr( $tooltip_text ),
     163            esc_attr( $tooltip_text )
     164        );
     165        echo '<span class="angie-snippet-info-icon">i</span>';
     166        echo '</span>';
    111167    }
    112168
     
    124180        }
    125181
     182        $post = get_post( $post_id );
     183        $snippet_slug = $post ? $post->post_name : '';
     184
    126185        $disabled_attr = $is_disabled ? ' disabled' : '';
    127         $button_text = ( $dev_time > 0 ) ? esc_html__( 'Publish to Live', 'angie' ) : esc_html__( 'Publish to Dev', 'angie' );
     186        $deploy_action = ( $dev_time > 0 ) ? 'push-to-production' : 'publish-to-dev';
     187        $button_text = ( $dev_time > 0 ) ? esc_html__( 'Push to Live', 'angie' ) : esc_html__( 'Push to Test', 'angie' );
    128188
    129189        printf(
    130             '<button type="button" class="button angie-push-to-production" data-post-id="%d"%s>%s</button>',
     190            '<button type="button" class="button angie-push-to-production" data-post-id="%d" data-snippet-slug="%s" data-action="%s"%s>%s</button>',
    131191            absint( $post_id ),
     192            esc_attr( $snippet_slug ),
     193            esc_attr( $deploy_action ),
    132194            esc_attr( $disabled_attr ),
    133195            esc_html( $button_text )
     
    144206            return;
    145207        }
    146 
    147         $script_url = plugins_url( 'assets/js/list-table-toggle.js', dirname( __FILE__ ) );
    148         wp_enqueue_script(
    149             'angie-list-table-toggle',
    150             $script_url,
    151             [],
    152             ANGIE_VERSION,
    153             true
    154         );
    155 
    156         wp_localize_script(
    157             'angie-list-table-toggle',
    158             'angieListTableToggle', [
    159                 'ajaxUrl' => admin_url( 'admin-ajax.php' ),
    160                 'nonce' => wp_create_nonce( 'angie_toggle_snippet_status' ),
    161                 'pushToProductionNonce' => wp_create_nonce( 'angie_push_to_production' ),
    162                 'i18n' => [
    163                     'publishToDevButton' => esc_html__( 'Publish to Dev', 'angie' ),
    164                     'confirmPublishToDev' => esc_html__( 'Are you sure you want to publish this snippet to dev?', 'angie' ),
    165                     'confirmPushToProduction' => esc_html__( 'Are you sure you want to push this snippet to production?', 'angie' ),
    166                     'pushing' => esc_html__( 'Pushing...', 'angie' ),
    167                     'failedToUpdateStatus' => esc_html__( 'Failed to update status', 'angie' ),
    168                     'errorOccurred' => esc_html__( 'An error occurred. Please try again.', 'angie' ),
    169                     'successPushedToProduction' => esc_html__( 'Successfully pushed to production', 'angie' ),
    170                     'failedToPushToProduction' => esc_html__( 'Failed to push to production', 'angie' ),
    171                 ],
    172             ]
    173         );
    174208
    175209        $style_url = plugins_url( 'assets/css/list-table-toggle.css', dirname( __FILE__ ) );
     
    182216    }
    183217
     218    public static function add_config( $config ) {
     219        $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
     220        if ( ! $screen || 'edit-' . Module::CPT_NAME !== $screen->id ) {
     221            return $config;
     222        }
     223
     224        if ( ! Module::current_user_can_manage_snippets() ) {
     225            return $config;
     226        }
     227
     228        $config['listTable'] = [
     229            'ajaxUrl'               => admin_url( 'admin-ajax.php' ),
     230            'nonce'                 => wp_create_nonce( 'angie_toggle_snippet_status' ),
     231            'pushToProductionNonce' => wp_create_nonce( 'angie_push_to_production' ),
     232        ];
     233
     234        return $config;
     235    }
     236
    184237    public static function ajax_toggle_status() {
    185238        check_ajax_referer( 'angie_toggle_snippet_status', 'nonce' );
     
    246299        } else {
    247300            $success = Dev_Mode_Manager::push_snippet_to_dev( $post_id );
    248             $success_message = esc_html__( 'Successfully published to dev', 'angie' );
     301            $success_message = esc_html__( 'Successfully published to test', 'angie' );
    249302        }
    250303
  • angie/trunk/modules/code-snippets/classes/rest-api-controller.php

    r3443255 r3464872  
    2020            [
    2121                [
    22                     'methods'             => \WP_REST_Server::READABLE,
    23                     'callback'            => [ $this, 'list_snippets' ],
    24                     'permission_callback' => [ $this, 'check_permission' ],
     22                    'methods' => \WP_REST_Server::READABLE,
     23                    'callback' => [ $this, 'list_snippets' ],
     24                    'permission_callback' => [ $this, 'check_permission' ],
     25                    'args'                => [
     26                        'type' => [
     27                            'required'          => false,
     28                            'type'              => 'string',
     29                            'sanitize_callback' => 'sanitize_text_field',
     30                        ],
     31                        'deployment_status' => [
     32                            'required'          => false,
     33                            'type'              => 'string',
     34                            'sanitize_callback' => 'sanitize_text_field',
     35                        ],
     36                    ],
    2537                ],
    2638            ]
     
    3244            [
    3345                [
    34                     'methods'             => \WP_REST_Server::READABLE,
    35                     'callback'            => [ $this, 'get_snippet' ],
    36                     'permission_callback' => [ $this, 'check_permission' ],
    37                     'args'                => [
     46                    'methods' => \WP_REST_Server::READABLE,
     47                    'callback' => [ $this, 'get_snippet' ],
     48                    'permission_callback' => [ $this, 'check_permission' ],
     49                    'args' => [
    3850                        'slug' => [
    39                             'required'          => true,
    40                             'type'              => 'string',
    41                             'sanitize_callback' => 'sanitize_text_field',
    42                         ],
    43                     ],
    44                 ],
    45                 [
    46                     'methods'             => \WP_REST_Server::DELETABLE,
    47                     'callback'            => [ $this, 'delete_snippet' ],
    48                     'permission_callback' => [ $this, 'check_permission' ],
    49                     'args'                => [
     51                            'required' => true,
     52                            'type' => 'string',
     53                            'sanitize_callback' => 'sanitize_text_field',
     54                        ],
     55                    ],
     56                ],
     57                [
     58                    'methods' => \WP_REST_Server::DELETABLE,
     59                    'callback' => [ $this, 'delete_snippet' ],
     60                    'permission_callback' => [ $this, 'check_permission' ],
     61                    'args' => [
    5062                        'slug' => [
    51                             'required'          => true,
    52                             'type'              => 'string',
     63                            'required' => true,
     64                            'type' => 'string',
    5365                            'sanitize_callback' => 'sanitize_text_field',
    5466                        ],
     
    6375            [
    6476                [
    65                     'methods'             => \WP_REST_Server::READABLE,
    66                     'callback'            => [ $this, 'list_snippet_files' ],
    67                     'permission_callback' => [ $this, 'check_permission' ],
    68                     'args'                => [
     77                    'methods' => \WP_REST_Server::READABLE,
     78                    'callback' => [ $this, 'list_snippet_files' ],
     79                    'permission_callback' => [ $this, 'check_permission' ],
     80                    'args' => [
    6981                        'slug' => [
    70                             'required'          => true,
    71                             'type'              => 'string',
    72                             'sanitize_callback' => 'sanitize_text_field',
    73                         ],
    74                     ],
    75                 ],
    76                 [
    77                     'methods'             => \WP_REST_Server::CREATABLE,
    78                     'callback'            => [ $this, 'upsert_snippet_files' ],
    79                     'permission_callback' => [ $this, 'check_permission' ],
    80                     'args'                => [
    81                         'slug'      => [
    82                             'required'          => true,
    83                             'type'              => 'string',
    84                             'sanitize_callback' => 'sanitize_text_field',
    85                         ],
    86                         'files'     => [
    87                             'required' => true,
    88                             'type'     => 'array',
     82                            'required' => true,
     83                            'type' => 'string',
     84                            'sanitize_callback' => 'sanitize_text_field',
     85                        ],
     86                    ],
     87                ],
     88                [
     89                    'methods' => \WP_REST_Server::CREATABLE,
     90                    'callback' => [ $this, 'upsert_snippet_files' ],
     91                    'permission_callback' => [ $this, 'check_permission' ],
     92                    'args' => [
     93                        'slug' => [
     94                            'required' => true,
     95                            'type' => 'string',
     96                            'sanitize_callback' => 'sanitize_text_field',
     97                        ],
     98                        'files' => [
     99                            'required' => true,
     100                            'type' => 'array',
    89101                        ],
    90102                        'overwrite' => [
    91103                            'required' => false,
    92                             'type'     => 'boolean',
    93                             'default'  => false,
    94                         ],
    95                         'type'      => [
    96                             'required'          => false,
    97                             'type'              => 'string',
     104                            'type' => 'boolean',
     105                            'default' => false,
     106                        ],
     107                        'type' => [
     108                            'required' => false,
     109                            'type' => 'string',
    98110                            'sanitize_callback' => 'sanitize_text_field',
    99111                        ],
     
    108120            [
    109121                [
    110                     'methods'             => \WP_REST_Server::READABLE,
    111                     'callback'            => [ $this, 'get_snippet_file' ],
    112                     'permission_callback' => [ $this, 'check_permission' ],
    113                     'args'                => [
    114                         'slug'     => [
    115                             'required'          => true,
    116                             'type'              => 'string',
     122                    'methods' => \WP_REST_Server::READABLE,
     123                    'callback' => [ $this, 'get_snippet_file' ],
     124                    'permission_callback' => [ $this, 'check_permission' ],
     125                    'args' => [
     126                        'slug' => [
     127                            'required' => true,
     128                            'type' => 'string',
    117129                            'sanitize_callback' => 'sanitize_text_field',
    118130                        ],
    119131                        'filename' => [
    120                             'required'          => true,
    121                             'type'              => 'string',
     132                            'required' => true,
     133                            'type' => 'string',
    122134                            'sanitize_callback' => 'sanitize_text_field',
    123135                        ],
     
    132144            [
    133145                [
    134                     'methods'             => \WP_REST_Server::CREATABLE,
    135                     'callback'            => [ $this, 'set_dev_mode' ],
    136                     'permission_callback' => [ $this, 'check_permission' ],
    137                     'args'                => [
     146                    'methods' => \WP_REST_Server::CREATABLE,
     147                    'callback' => [ $this, 'set_dev_mode' ],
     148                    'permission_callback' => [ $this, 'check_permission' ],
     149                    'args' => [
    138150                        'enabled' => [
    139                             'required'          => true,
    140                             'type'              => 'boolean',
     151                            'required' => true,
     152                            'type' => 'boolean',
    141153                            'sanitize_callback' => 'rest_sanitize_boolean',
    142154                        ],
     
    151163        [
    152164            [
    153                 'methods'             => \WP_REST_Server::CREATABLE,
    154                 'callback'            => [ $this, 'publish_snippet' ],
     165                'methods' => \WP_REST_Server::CREATABLE,
     166                'callback' => [ $this, 'publish_snippet' ],
    155167                'permission_callback' => [ $this, 'check_permission' ],
    156                 'args'                => [
     168                'args' => [
    157169                    'slug' => [
    158                         'required'          => true,
    159                         'type'              => 'string',
     170                        'required' => true,
     171                        'type' => 'string',
    160172                        'sanitize_callback' => 'sanitize_text_field',
    161173                    ],
     
    170182            [
    171183                [
    172                     'methods'             => \WP_REST_Server::CREATABLE,
    173                     'callback'            => [ $this, 'validate_snippet' ],
    174                     'permission_callback' => [ $this, 'check_permission' ],
    175                     'args'                => [
     184                    'methods' => \WP_REST_Server::CREATABLE,
     185                    'callback' => [ $this, 'validate_snippet' ],
     186                    'permission_callback' => [ $this, 'check_permission' ],
     187                    'args' => [
    176188                        'files' => [
    177189                            'required' => true,
    178                             'type'     => 'array',
     190                            'type' => 'array',
    179191                        ],
    180192                    ],
     
    187199            [
    188200                [
    189                     'methods'             => \WP_REST_Server::READABLE,
    190                     'callback'            => [ $this, 'is_dev_mode' ],
     201                    'methods' => \WP_REST_Server::READABLE,
     202                    'callback' => [ $this, 'is_dev_mode' ],
    191203                    'permission_callback' => [ $this, 'check_permission' ],
    192204                ],
     
    200212
    201213    public function list_snippets( $request ) {
    202         $posts = Snippet_Repository::get_all_snippets();
     214        $type = $request->get_param( 'type' );
     215        $deployment_status_param = $request->get_param( 'deployment_status' );
     216        $deployment_statuses = $deployment_status_param ? array_map( 'trim', explode( ',', $deployment_status_param ) ) : [];
     217
     218        $posts = Snippet_Repository::get_all_snippets( $type );
    203219
    204220        $snippets = [];
    205221        foreach ( $posts as $post ) {
    206             $snippets[] = Snippet_Repository::get_snippet_data( $post );
     222            $snippet_data = Snippet_Repository::get_snippet_data( $post );
     223
     224            if ( ! empty( $deployment_statuses ) && ! in_array( $snippet_data['deploymentStatus'], $deployment_statuses, true ) ) {
     225                continue;
     226            }
     227
     228            $snippets[] = $snippet_data;
    207229        }
    208230
     
    249271
    250272    public function get_snippet_file( $request ) {
    251         $slug     = $request->get_param( 'slug' );
     273        $slug = $request->get_param( 'slug' );
    252274        $filename = $request->get_param( 'filename' );
    253         $post     = Snippet_Repository::find_snippet_post_by_slug( $slug );
     275        $post = Snippet_Repository::find_snippet_post_by_slug( $slug );
    254276
    255277        if ( ! $post ) {
     
    278300
    279301        return rest_ensure_response( [
    280             'name'    => $file['name'],
     302            'name' => $file['name'],
    281303            'content' => $content,
    282             'size'    => strlen( $content ),
     304            'size' => strlen( $content ),
    283305        ] );
    284306    }
    285307
    286308    public function upsert_snippet_files( $request ) {
    287         $slug      = $request->get_param( 'slug' );
    288         $files     = $request->get_param( 'files' );
     309        $slug = $request->get_param( 'slug' );
     310        $files = $request->get_param( 'files' );
    289311        $overwrite = $request->get_param( 'overwrite' );
    290         $type      = $request->get_param( 'type' );
     312        $type = $request->get_param( 'type' );
    291313
    292314        if ( ! is_array( $files ) || empty( $files ) ) {
     
    417439            return rest_ensure_response( [
    418440                'success' => true,
     441                'created' => true,
    419442                'message' => esc_html__( 'Snippet created successfully.', 'angie' ),
    420                 'slug'    => $slug,
    421                 'files'   => count( $sanitized_files ),
     443                'slug' => $slug,
     444                'post_id' => $post_id,
     445                'files' => count( $sanitized_files ),
    422446            ] );
    423447        }
     
    452476        return rest_ensure_response( [
    453477            'success' => true,
     478            'created' => false,
    454479            'message' => esc_html__( 'Snippet files updated successfully.', 'angie' ),
    455             'slug'    => $slug,
    456             'files'   => count( $merged_files ),
     480            'slug' => $slug,
     481            'post_id' => $post->ID,
     482            'files' => count( $merged_files ),
    457483        ] );
    458484    }
     
    470496        }
    471497
    472     $environments = [ Dev_Mode_Manager::ENV_DEV, Dev_Mode_Manager::ENV_PROD ];
    473     File_System_Handler::delete_snippet_files( $post->ID, $environments );
    474 
    475     $result = Snippet_Repository::delete_snippet( $post->ID );
    476 
    477     if ( ! $result ) {
    478         return new \WP_Error(
    479             'delete_failed',
    480             esc_html__( 'Failed to delete snippet.', 'angie' ),
    481             [ 'status' => 500 ]
    482         );
    483     }
    484 
    485     Cache_Manager::clear_published_snippet_cache();
    486 
    487     return rest_ensure_response( [
    488         'success' => true,
    489         'message' => esc_html__( 'Snippet deleted successfully.', 'angie' ),
    490         'slug'    => $slug,
    491     ] );
    492   }
    493 
    494     public function set_dev_mode( $request ) {
    495         $enabled = $request->get_param( 'enabled' );
    496 
    497         if ( $enabled ) {
    498             $session = Dev_Mode_Manager::create_dev_mode_session();
    499 
    500             if ( ! $session ) {
    501                 return new \WP_Error(
    502                     'session_creation_failed',
    503                     esc_html__( 'Failed to create dev mode session. User must be logged in.', 'angie' ),
    504                     [ 'status' => 403 ]
    505                 );
    506             }
    507 
    508             return rest_ensure_response( [
    509                 'success' => true,
    510                 'message' => esc_html__( 'Dev mode enabled for 1 hour.', 'angie' ),
    511                 'enabled' => true,
    512                 'expiry'  => $session['expiry'],
    513             ] );
    514         } else {
    515             Dev_Mode_Manager::clear_dev_mode_session();
    516 
    517             return rest_ensure_response( [
    518                 'success' => true,
    519                 'message' => esc_html__( 'Dev mode disabled.', 'angie' ),
    520                 'enabled' => false,
    521             ] );
    522         }
    523     }
    524 
    525     public function publish_snippet( $request ) {
    526         $slug = $request->get_param( 'slug' );
    527         $post = Snippet_Repository::find_snippet_post_by_slug( $slug );
    528 
    529         if ( ! $post ) {
    530             return new \WP_Error(
    531                 'snippet_not_found',
    532                 esc_html__( 'Snippet not found.', 'angie' ),
    533                 [ 'status' => 404 ]
    534             );
    535         }
    536 
    537         $files = Snippet_Repository::get_snippet_files( $post->ID );
    538 
    539         if ( empty( $files ) ) {
    540             return new \WP_Error(
    541                 'no_files',
    542                 esc_html__( 'Snippet has no files to publish.', 'angie' ),
    543                 [ 'status' => 400 ]
    544             );
    545         }
    546 
    547         File_System_Handler::write_snippet_files_to_disk( Dev_Mode_Manager::ENV_PROD, $post->ID, $files );
    548         Cache_Manager::clear_published_snippet_cache();
    549 
    550         return rest_ensure_response( [
    551             'success' => true,
    552             'message' => esc_html__( 'Snippet published to production successfully.', 'angie' ),
    553             'slug'    => $slug,
    554             'files'  => count( $files ),
    555         ] );
    556     }
     498        $environments = [ Dev_Mode_Manager::ENV_DEV, Dev_Mode_Manager::ENV_PROD ];
     499        File_System_Handler::delete_snippet_files( $post->ID, $environments );
     500
     501        $result = Snippet_Repository::delete_snippet( $post->ID );
     502
     503        if ( ! $result ) {
     504            return new \WP_Error(
     505                'delete_failed',
     506                esc_html__( 'Failed to delete snippet.', 'angie' ),
     507                [ 'status' => 500 ]
     508            );
     509        }
     510
     511        Cache_Manager::clear_published_snippet_cache();
     512
     513        return rest_ensure_response( [
     514            'success' => true,
     515            'message' => esc_html__( 'Snippet deleted successfully.', 'angie' ),
     516            'slug' => $slug,
     517        ] );
     518    }
     519
     520    public function set_dev_mode( $request ) {
     521        $enabled = $request->get_param( 'enabled' );
     522
     523        if ( $enabled ) {
     524            $session = Dev_Mode_Manager::create_dev_mode_session();
     525
     526            if ( ! $session ) {
     527                return new \WP_Error( 'session_creation_failed',
     528                    esc_html__( 'Failed to create test mode session. User must be logged in.', 'angie' ),
     529                    [ 'status' => 403 ]
     530                );
     531            }
     532
     533            return rest_ensure_response( [
     534                'success' => true,
     535                'message' => esc_html__( 'Test mode enabled.', 'angie' ),
     536                'enabled' => true,
     537                'expiry'  => $session['expiry'],
     538            ] );
     539        } else {
     540            Dev_Mode_Manager::clear_dev_mode_session();
     541
     542            return rest_ensure_response( [
     543                'success' => true,
     544                'message' => esc_html__( 'Test mode disabled.', 'angie' ),
     545                'enabled' => false,
     546            ] );
     547        }
     548    }
     549
     550    public function publish_snippet( $request ) {
     551        $slug = $request->get_param( 'slug' );
     552        $post = Snippet_Repository::find_snippet_post_by_slug( $slug );
     553
     554        if ( ! $post ) {
     555            return new \WP_Error(
     556                'snippet_not_found',
     557                esc_html__( 'Snippet not found.', 'angie' ),
     558                [ 'status' => 404 ]
     559            );
     560        }
     561
     562        $files = Snippet_Repository::get_snippet_files( $post->ID );
     563
     564        if ( empty( $files ) ) {
     565            return new \WP_Error(
     566                'no_files',
     567                esc_html__( 'Snippet has no files to publish.', 'angie' ),
     568                [ 'status' => 400 ]
     569            );
     570        }
     571
     572        File_System_Handler::write_snippet_files_to_disk( Dev_Mode_Manager::ENV_PROD, $post->ID, $files );
     573        Cache_Manager::clear_published_snippet_cache();
     574
     575        return rest_ensure_response( [
     576            'success' => true,
     577            'message' => esc_html__( 'Snippet published to production successfully.', 'angie' ),
     578            'slug' => $slug,
     579            'post_id' => $post->ID,
     580            'files' => count( $files ),
     581        ] );
     582    }
    557583
    558584    public function validate_snippet( $request ) {
  • angie/trunk/modules/code-snippets/classes/snippet-repository.php

    r3443255 r3464872  
    7070    }
    7171
    72     public static function get_all_snippets() {
     72    public static function get_all_snippets( $type = null ) {
    7373        $args = [
    7474            'post_type'      => Module::CPT_NAME,
     
    7878            'order'          => 'ASC',
    7979        ];
     80
     81        if ( ! empty( $type ) && Taxonomy_Manager::is_valid_type( $type ) ) {
     82            $args['tax_query'] = [
     83                [
     84                    'taxonomy' => Taxonomy_Manager::TAXONOMY_NAME,
     85                    'field'    => 'slug',
     86                    'terms'    => $type,
     87                ],
     88            ];
     89        }
    8090
    8191        return get_posts( $args );
     
    95105    public static function get_snippet_data( $post ) {
    96106        $files = self::get_snippet_files( $post->ID );
     107        $terms = wp_get_object_terms( $post->ID, Taxonomy_Manager::TAXONOMY_NAME, [ 'fields' => 'slugs' ] );
     108        $timestamps = Dev_Mode_Manager::get_snippet_environment_timestamps( $post->ID );
     109        $is_elementor_widget = ! is_wp_error( $terms ) && in_array( 'elementor-widget', $terms, true );
    97110
    98         return [
     111        $data = [
     112            'id'     => $post->ID,
    99113            'slug'   => self::get_snippet_slug_from_post( $post ),
    100114            'title'  => $post->post_title,
    101115            'status' => $post->post_status,
    102116            'files'  => self::build_file_list( $files ),
     117            'type'   => is_wp_error( $terms ) ? [] : $terms,
     118            'deploymentStatus' => $timestamps['status'],
    103119        ];
     120
     121        if ( $is_elementor_widget ) {
     122            $data['widgetName'] = Widget_Name_Resolver::get_widget_name_for_snippet( $post->ID );
     123        }
     124
     125        return $data;
    104126    }
    105127
  • angie/trunk/modules/code-snippets/classes/snippet-validator.php

    r3443255 r3464872  
    4545        }
    4646
     47        $validate_url = self::get_loopback_url( 'angie/v1/snippets/validate' );
    4748        $response = wp_remote_post(
    48             rest_url( 'angie/v1/snippets/validate' ),
     49            $validate_url,
    4950            [
    5051                'headers' => [
     
    7980
    8081        if ( \WP_Http::OK !== $response_code || ! isset( $validation_result['valid'] ) || ! $validation_result['valid'] ) {
    81             $error_message = isset( $validation_result['message'] ) ? $validation_result['message'] : esc_html__( 'Unknown validation error.', 'angie' );
     82            $error_message = $validation_result['data']['error']['message']
     83                ?? $validation_result['data']['details']['message']
     84                ?? $validation_result['message']
     85                ?? esc_html__( 'Unknown validation error.', 'angie' );
     86
    8287            return new \WP_Error(
    8388                'validation_failed',
     
    144149
    145150        try {
     151
    146152            include $file_path;
    147153
     
    183189    }
    184190
     191    // This loopback url is used to validate the snippet in the local environment (wp-env)
     192    private static function get_loopback_url( $rest_route ) {
     193        $url = rest_url( $rest_route );
     194
     195        $host = wp_parse_url( $url, PHP_URL_HOST );
     196
     197        if ( 'localhost' !== $host && '127.0.0.1' !== $host ) {
     198            return $url;
     199        }
     200
     201        return str_replace( $host, 'host.docker.internal', $url );
     202    }
     203
    185204    private static function create_temp_snippet_dir() {
    186205        $temp_base = WP_CONTENT_DIR . '/angie-snippets/temp';
  • angie/trunk/modules/code-snippets/module.php

    r3443255 r3464872  
    3636
    3737        add_action( 'rest_api_init', [ $this, 'register_rest_routes' ] );
     38        add_action( 'wp_logout', [ Dev_Mode_Manager::class, 'clear_dev_mode_session' ] );
    3839
    3940        if ( $this->should_load_snippets() ) {
     
    7980
    8081    private function init_components() {
     82        require_once __DIR__ . '/utils.php';
     83
    8184        Post_Type_Manager::init();
    8285        Taxonomy_Manager::init();
     
    9699
    97100    public static function is_active(): bool {
    98         return defined( 'ANGIE_CODE_SNIPPETS_ACTIVE' ) && ANGIE_CODE_SNIPPETS_ACTIVE;
     101        return true;
    99102    }
    100103
  • angie/trunk/modules/elementor-core/module.php

    r3363303 r3464872  
    4141        $this->init_rest_controllers();
    4242        add_action( 'elementor/editor/after_enqueue_scripts', [ $this, 'enqueue_scripts' ] );
     43        add_action( 'elementor/elements/categories_registered', [ $this, 'register_widget_categories' ] );
     44        add_action( 'elementor/editor/templates/panel/category', [ $this, 'render_angie_category_generate_button' ] );
     45        add_action( 'elementor/editor/templates/panel/category/content', [ $this, 'render_angie_category_empty_state' ] );
    4346        add_filter( 'angie_mcp_plugins', function ( $plugins ) {
    4447            $plugins['elementor'] = [];
     
    4750    }
    4851
    49     /**
    50      * Initialize controllers
    51      */
     52    public function register_widget_categories( $elements_manager ) {
     53        $elements_manager->add_category(
     54            'angie-widgets',
     55            [
     56                'title' => esc_html__( 'Angie Widgets', 'angie' ),
     57                'icon' => 'eicon-ai',
     58                'hideIfEmpty' => false,
     59                'active' => true,
     60            ]
     61        );
     62    }
     63
     64    public function render_angie_category_generate_button() {
     65        ?><# if ( 'angie-widgets' === name ) { #>
     66        <span class="angie-category-generate" data-angie-generate-widget style="display: inline-flex; align-items: center; gap: 4px; margin-inline-start: auto; color: #C00BB9; color: light-dark(#C00BB9, #F0ABFC); font-size: 12px; font-weight: 500; cursor: pointer;">
     67            <i class="eicon-ai" aria-hidden="true" style="font-size: 14px;"></i>
     68            <?php echo esc_html__( 'Generate', 'angie' ); ?>
     69        </span>
     70        <# } #><?php
     71    }
     72
     73    public function render_angie_category_empty_state() {
     74        if ( $this->has_angie_widgets() ) {
     75            return;
     76        }
     77        ?><# if ( 'angie-widgets' === name ) { #>
     78        <div class="angie-category-empty-state" data-angie-category-empty-state style="grid-column: 1 / -1; width: 100%; padding: 12px 20px;">
     79            <p style="color: #A4AFB7; font-size: 12px; margin: 0; line-height: 1.4;"><?php echo esc_html__( 'Your generated widgets by Angie will show up here.', 'angie' ); ?></p>
     80        </div>
     81        <# } #><?php
     82    }
     83
     84    private function has_angie_widgets(): bool {
     85        $widgets = \Elementor\Plugin::$instance->widgets_manager->get_widget_types();
     86
     87        foreach ( $widgets as $widget ) {
     88            if ( in_array( 'angie-widgets', $widget->get_categories(), true ) ) {
     89                return true;
     90            }
     91        }
     92
     93        return false;
     94    }
     95
    5296    private function init_rest_controllers() {
    5397        $this->kit_provider = new Kit_Provider();
  • angie/trunk/modules/sidebar/assets/sidebar.css

    r3379381 r3464872  
    1818    body.angie-sidebar-active #angie-body-top-padding {
    1919        width: 100%;
    20         height: 8px;
     20        height: 0;
    2121    }
    2222
     
    2424    body.angie-sidebar-active #wpadminbar {
    2525        inset-inline-start: var(--angie-sidebar-width) !important;
    26         inset-inline-end: 8px !important;
    27         width: calc(100% - 8px - var(--angie-sidebar-width)) !important;
    28         margin-top: 8px;
     26        inset-inline-end: 0 !important;
     27        width: calc(100% - var(--angie-sidebar-width)) !important;
     28        margin-top: 0;
    2929    }
    3030
    3131    body.angie-sidebar-active #wpfooter {
    32         bottom: 8px !important;
     32        bottom: 0 !important;
    3333    }
    3434
    3535    body.angie-sidebar-active #adminmenuback {
    36         top: 8px !important;
     36        top: 0 !important;
    3737    }
    3838
    3939    html.angie-sidebar-active {
    40         margin-inline-end: 8px !important;
     40        margin-inline-end: 0 !important;
    4141    }
    4242
    4343    body.angie-sidebar-active #adminmenuwrap {
    44         margin-bottom: 8px;
     44        margin-bottom: 0;
    4545    }
    4646
    47     body.angie-sidebar-active #angie-wrapper {
    48         --angie-wrapper-border-width: 1px;
    49         position: fixed;
    50         top: 8px;
    51         inset-inline-start: var(--angie-sidebar-width);
    52         width: calc(100% - 7px - (var(--angie-wrapper-border-width) * 2) - var(--angie-sidebar-width));
    53         height: calc(100% - 17px);
    54         z-index: var(--angie-sidebar-z-index) !important;
    55         border: var(--angie-wrapper-border-width) solid var(--action-selected, rgba(0, 0, 0, 0.08));
    56         border-radius: 8px;
    57         box-shadow: 0 0 0 100vmax #FCFCFC;
    58         pointer-events: none;
    59     }
    60 
    61 
    6247    body.angie-sidebar-active .woocommerce-layout__header{
    63         margin-top: 8px;
     48        margin-top: 0;
    6449    }
    6550
     
    9984    inset-inline-start: calc(var(--angie-sidebar-width) + var(--admin-menu-width)) !important;
    10085    inset-inline-end: 0 !important;
    101     top: calc(32px + 8px) !important;
    102     margin-bottom: 8px !important;
     86    top: 32px !important;
     87    margin-bottom: 0 !important;
    10388}
    10489
    10590.block-editor-page.angie-sidebar-active.block-editor-page.is-fullscreen-mode .interface-interface-skeleton {
    106     top: 8px !important;
     91    top: 0 !important;
    10792}
    10893
  • angie/trunk/modules/sidebar/components/sidebar-html.php

    r3385659 r3464872  
    9191        </script>
    9292
    93         <div id='angie-wrapper'></div>
    9493        <div
    9594            id='angie-sidebar-container'
  • angie/trunk/plugin.php

    r3437542 r3464872  
    9191        }
    9292        $this->init();
     93        $this->register_heartbeat_nonce_refresh();
     94    }
     95
     96    private function register_heartbeat_nonce_refresh() {
     97        add_filter( 'heartbeat_received', [ $this, 'refresh_angie_nonce_on_heartbeat' ], 10, 2 );
     98    }
     99
     100    public function refresh_angie_nonce_on_heartbeat( array $response, array $data ): array {
     101        if ( ! is_user_logged_in() ) {
     102            return $response;
     103        }
     104        $response['angie_nonce'] = wp_create_nonce( 'wp_rest' );
     105        return $response;
    93106    }
    94107}
  • angie/trunk/readme.txt

    r3443255 r3464872  
    55Tested up to: 6.9
    66Requires PHP: 7.4
    7 Stable tag: 1.0.4
     7Stable tag: 1.1.0
    88License: GPLv3
    99License URI: https://www.gnu.org/licenses/gpl-3.0.html
    1010
    11 Chat with Angie, Agentic AI for WordPress. Powered by MCP, Angie builds, manages & updates your site, boosting productivity with ease.
     11Angie Code: Your expert WordPress developer, powered by AI. Build anything you can imagine without writing a single line of code.
    1212
    1313== Description ==
    1414
    15 **Disclaimer:** Angie is currently in a Beta mode. While core functionality in WordPress and the Elementor Editor is stable and ready to explore, some integrations with other plugins and tools are still evolving and may not work as expected yet. We're continuously improving Angie and expanding these integrations, so you can expect broader, more refined capabilities soon. In the meantime, be sure to back up your site before using Angie.
    16 
    17 Manage and update your website by chatting with Angie, a powerful Agentic AI for any WordPress website. With your instruction, Angie can perform simple or complex multi-step workflows and bulk actions in an instant, to multiply your productivity across any part of website creation.
    18 
    19 Unlike traditional AI tools or AI plugins that only generate content or assist with isolated tasks, Angie acts as an intelligent AI assistant that powers your full WordPress site flow, from content and design to code, and site maintenance, without any manual tagging or training. Perfect for professional web creators, agencies, freelancers, and professional site owners who want to work smarter and achieve more.
    20 
    21 Capture the power of MCP (Model Context Protocol) technology and autonomous AI Agents to carry out multi-step tasks like updating pages, installing plugins, bulk uploading new inventory to your WooCommerce store, or generating entire landing pages, all executed intelligently and in context. Angie frees you from repetitive admin work so you can focus on strategy and creativity, while your AI assistant runs your WordPress site seamlessly.
    22 
    23 == Use Cases ==
    24 
    25 Angie performs hundreds of everyday WordPress tasks, spanning content, design, development, and store management. Below are just some of the operations Angie can streamline for web creators:
    26 
    27 = Content Management =
    28 
    29 - **Bulk content creation** - Quickly generate or duplicate multiple posts, products, or pages in batches.
    30 - **Upload content** - Add images, PDFs or other media from local files or URLs, letting Angie handle the upload and attachment process.
    31 - **Generate content based on web searches** - Pull in up-to-date facts and references from the web to draft posts, product descriptions, and page copy aligned to your brief.
    32 - **AI image generation or editing** (using Angie's Image Generation Mini App) - Generate new images or edit existing ones (remove or replace background, resize, upscale, generative fill, isolate objects, cleanup) without leaving WordPress.
    33 - **Container generation** (requires Elementor's Editor) - Optimize container layouts, automatically placing elements in the appropriate container.
    34 - **Scheduling posts & applying categories and tags** - Fill in unpublished items with smart scheduling and auto-apply categories or tags based on content meaning for better organization and SEO.
    35 - **Create Custom Post Types with ACF®** (Current status: experimental) - Define and register new post types tailored to specific content structures, like events, portfolios, or testimonials.
    36 - **Full page content translation** - Translate individual blocks, full pages, or any piece of content across the site without relying on any external tools.
    37 - **Generate full pages** - Using the Page Planner, describe the type of page needed (e.g. "About page for a bakery") and let Angie create it with the layout, content, and structure included.
    38 
    39 = Design & Development =
    40 
    41 - **Update global styles** - Instantly adjust typography, colors, spacing, and other theme-wide design settings with just a single prompt.
    42 - **Code creation** - Write or insert code snippets (HTML, CSS, JS) for site customization.
    43 - **Improve slugs** - Refine post, page, or product URLs for better SEO and readability, either individually or in bulk.
    44 
    45 = Site Management =
    46 
    47 - **Plugin & Theme operations** - Activate, deactivate, install, or remove plugins and themes with a single prompt.
    48 - **Site configuration** - Adjust settings like site title, tagline, site language, timezone, or reading preferences.
    49 - **User roles & access** - Add new users, update roles, or adjust permissions through conversational commands.
    50 - **Media library control** - Enhance media assets with AI-generated alt text, captions and descriptions, or quickly search, rename, organize, and clean up files in bulk using simple prompts.
    51 
    52 = WooCommerce Management =
    53 
    54 - **Create and manage products** - Generate detailed WooCommerce product listings with descriptions, pricing, categories, and images.
    55 - **Stock management** - Update inventory levels, set stock statuses, and adjust quantity settings across multiple products.
    56 - **Pricing adjustments** - Quickly update regular and sale prices, either individually or in bulk, across multiple products.
    57 - **Promotions & discounts** - Apply promotional discounts, set up coupon codes, or schedule limited-time sales with a single prompt.
    58 
    59 = Analytics & Reporting (Coming soon) =
    60 
    61 - **Product performance reports** - Get instant snapshots of sales volume, revenue trends, and top-selling products.
    62 - **Order & product management** - Ask Angie to manage fulfillment, stock updates, order statuses, and product details.
    63 
    64 = More ways Angie can help you =
    65 
    66 - **Site recommendations** - Get step-by-step instructions for WordPress tasks or best-practice recommendations.
    67 - **How-to assistance** - Ask Angie how to use the Editor, WordPress, or any plugin and get clear, actionable instructions.
    68 - **Ideation partner** - Brainstorm ideas for new content, campaigns, or site features with Angie.
    69 - **Plugin & theme advice** - Discover which plugins or themes can extend your site with the functionality you need.
    70 - **Hosting & performance tips** - Ask Angie about site health, performance issues, or hosting configurations.
    71 
    72 == Key Benefits ==
    73 
    74 - **Agentic AI - Multi-step workflow automation** - From a single prompt, Angie completes complex, multi-step tasks. Whether it's creating pages, updating inventory, or publishing bulk content, Angie handles it seamlessly within your site.
    75 - **Sitewide next-generation AI engine** - Angie works across your entire WordPress site, automating content, design, code, plugin management, and maintenance workflows. It delivers a unified AI-powered engine for any WordPress website, even if you're not using Elementor's Editor.
    76 - **AI for your entire website journey** - Angie supports you from planning and building to launching and maintaining your WordPress site. It helps you build, run, and grow your site with smart automation at every stage.
    77 - **Deep context awareness with MCP technology** - Angie instantly understands your site's structure, including pages, posts, WooCommerce products, plugins, and templates. This deep site-level intelligence powers precise, context-aware actions without manual tagging or setup.
    78 - **Intelligent AI assistance** - Angie performs complex tasks quickly and accurately. It can install plugins, summarize data, manage updates, and reduce repetitive admin work, making site management easier and more efficient.
    79 - **Easy AI workflow control** - Manage complex tasks with simple commands. Angie brings content, design, code, data, and backend admin actions together in one place, so you can run your whole site without juggling tools.
    80 - **Built for professionals and continuous growth** - Designed for agencies, freelancers, and professional web creators managing multiple sites. Angie streamlines workflows and supports ongoing updates like sales launches, store inventory management, and maintenance, helping your site grow with minimal effort.
    81 - **Zero setup, seamless integration** - Install and go. Angie works with your existing site as-is. No training or manual configuration is needed. Start automating workflows immediately.
    82 
    83 == Features ==
    84 
    85 - **Prompt-based task execution** - Issue a single prompt, and Angie translates it into a structured, multi-step workflow with no scripting or manual setup required.
    86 - **Context-aware content mapping** - Built to understand WordPress architecture, Angie automatically determines where each action belongs across pages, templates, products, and more.
    87 - **Multi-modal output generation** - Angie is capable of producing diverse outputs (including text, code, images, or layouts) within the same interface, all triggered from one command.
    88 - **Flexible input options** - Angie supports multiple input sources, such as uploading files, images, or providing URLs for reference.
    89 - **Data-driven actions** - Angie accepts structured files like CSVs or JSON to drive large-scale updates, content imports, or data manipulations across your site.
    90 - **Plugin & theme automation** - Since plugin and theme management is built into Angie's workflow system, installation and configuration can happen automatically, without any manual steps required.
    91 - **Web search integration** - With built-in web search, Angie can retrieve and process real-time information to support tasks like content creation or analysis.
    92 - **Deep Elementor integration** - The integration with Elementor is handled natively, giving Angie full access to design components, layout structures, and editor actions.
    93 
    94 == Roadmap: Expanding next-generation AI capabilities ==
    95 
    96 Angie is continuously evolving to deliver smarter, deeper AI automation for WordPress site management. Upcoming updates will include:
    97 
    98 - **Background task scheduling** - Run complex or time-intensive tasks in the background, even when you're offline.
    99 - **Custom agents** - Create and assign specialized abilities for Angie to interact with your site in new ways.
    100 - **Integration hub** - Easily connect external MCP servers or built-in solutions such as the Figma connector from a dedicated screen.
    101 - **Expanded plugin integration** - Expansion of integration across more plugins including WooCommerce, The Events Calendar and more.
    102 
    103 These enhancements will empower professional web creators, agencies, and freelancers to automate complex site-wide tasks effortlessly, streamline workflows, and unlock the full potential of AI-powered WordPress site management.
    104 
    105 == Get started today ==
    106 
    107 Experience the power of Angie, your context-aware AI assistant for WordPress!
    108 
    109 Angie requires a connection to an active Elementor account to identify the user and provide access to the AI-powered services to their WordPress site. Make sure you are signed in with an Elementor user account. Upon activating the plugin, you will be guided to connect your site. Learn more about Angie's Terms and Conditions.
    110 
    111 More exciting features and improvements are coming soon, so stay tuned!
     15**Disclaimer:** Angie is currently in Beta. While core functionality within WordPress and the Elementor Editor is stable and ready to explore, some actions and integrations with third-party tools are still evolving. We are continuously refining Angie's capabilities and expanding its capabilities. Please ensure you back up your site before use. During this Beta phase, enjoy free daily credits and help us shape the future of AI-driven web creation with your feedback.
     16
     17Angie Code lets professional web creators and agencies create custom Elementor widgets, snippets for WordPress, and functionality instantly. Simply describe what you need, and watch Angie build production-ready code in seconds, zero coding knowledge required.
     18
     19While standard solutions provide a vital foundation for most project requirements, Angie Code eliminates the compromise of "close enough", empowering you to build the bespoke functionalities that define the final, most impactful details of your site ensuring every element meets your exact specifications with technical precision.
     20
     21== Fully configurable inside WordPress & Elementor ==
     22
     23Angie produces PHP, CSS, and JavaScript, whose output is editable through conversation with Angie ("make the font bigger," "add a hover animation"), or switch to the Elementor Editor's visual controls for pixel-perfect adjustments. You're never locked in - every widget is yours to own, edit, and evolve.
     24
     25Your existing theme, plugins, Elementor setup, and Gutenberg editor all stay exactly as they are. Angie adds AI-powered creation capabilities on top of your current environment. There is no new platform to learn, no migration required, and no disruption to your workflow.
     26
     27== What Angie can do ==
     28
     29Angie's expertise delivers specialized categories of production-ready components:
     30
     31- **Build bespoke Elementor widgets** - Create custom widgets from scratch and refine their style using native Elementor Editor controls.
     32- **Extend Elementor widgets** - Inject custom controls and advanced capabilities into existing widgets to expand their functional range.
     33- **Customize WordPress admin & backend** - Tailor your dashboard, add custom post types, extend WooCommerce, and implement custom hooks and filters.
     34- **Architect front-end enhancements** - Integrate CSS and JavaScript for sophisticated animations, cursor effects, and immersive interactive behaviors.
     35- **Construct visual apps** - Build fully functional front-end applications like custom calculators, and quizzes.
     36- **Generate complete landing pages** - Produce entire pages with layout, content, and styling for Elementor and Gutenberg.
     37
     38== Multi-modal input and collaborative building ==
     39
     40- **Conversational building** - Describe the functionality you need in plain language; Angie handles the engineering.
     41- **Visual references** - Upload a screenshot of a design you love, or provide a URL, and your AI developer will build it for you.
     42- **Collaborative refinement** - Not exactly what you wanted? Just tell Angie what to change. She understands your feedback and refines the result in real-time.
     43
     44== Safe by design - experiment freely, deploy confidently ==
     45
     46Everything Angie Code creates is built and tested in a dedicated preview environment. See it on your page, tweak it, and iterate as many times as you want - nothing is added to your live site until you personally approve it. This is your safety net: the freedom to try anything without the risk of breaking anything.
     47
     48== Streamline site management ==
     49
     50Beyond building, Angie acts as an agentic assistant to help you perform site-wide workflows, handle bulk actions, and manage content through simple conversation.
     51
     52Stop compromising your vision. Start building with Angie.
     53
     54== Contributors & Developers ==
     55
     56"Angie: Agentic AI for WordPress" is open source software. The following people have contributed to this plugin.
     57
     58Contributors: Elementor
    11259
    11360== FAQ ==
    11461
    115 = 1. What is Angie and how does it work? =
    116 
    117 Angie is a next-generation powerful Agentic AI assistant for your WordPress website that enables you to manage and update your website using conversational prompts. It understands your entire site's structure, including pages, posts, templates, and the products you sell in your WooCommerce store. Angie also knows all the widgets and plugins you use and can install new plugins for you. It has deep knowledge of where everything is inside WordPress. Using simple natural language prompts, Angie performs complex multi-step tasks on your behalf, helping you automate updates, content generation, design changes, and much more so you can multiply your productivity. This saves you time and effort, while making site management easier and more efficient.
    118 
    119 = 2. Is Angie compatible with WordPress and Elementor? =
    120 
    121 Yes. Angie is fully compatible with WordPress and seamlessly integrates with Elementor's Editor, though it is not required. Angie is also integrated with other themes and plugins, such as Gutenberg (The WordPress Block Editor), The Events Calendar, GiveWP. It works across your entire site, delivering powerful AI-driven automation beyond just page builders.
    122 
    123 = 3. Do I need to configure, train, or restructure my existing WordPress site for Angie to work? =
    124 
    125 No, Angie requires no configuration, manual training, or site restructuring to get started. Once installed and connected, Angie automatically recognizes your site's layout, content types, plugins, and even your WooCommerce products. There's no tagging, manual setup, or complicated process required. Angie seamlessly adapts to your existing WordPress site as-is, allowing you to open its interface and immediately start issuing natural language commands to automate complex, multi-step tasks.
    126 
    127 = 4. Will Angie complicate my workflow? =
    128 
    129 Not at all. Angie simplifies your workflow by unifying AI-powered content, design, and code automation into one intelligent interface. With text commands and deep site context awareness, Angie helps you move faster from idea to execution with less friction.
    130 
    131 = 5. Do you need to use Elementor's Editor for Angie to work? =
    132 
    133 No, Angie works directly in WordPress and is fully compatible with various plugins. You don't need Elementor to use it. However, Angie has deep integration with Elementor's Editor, giving it extra context and control over widgets, templates, and design structure. If you're using Elementor, you'll get the most out of Angie — but it also works smoothly with Gutenberg and other WordPress environments.
    134 
    135 = 6. Is Angie free? =
    136 
    137 Yes, Angie has a free tier with credits renewing daily.
    138 
    139 = 7. Is my site secure with Angie? =
    140 
    141 Yes. Angie is built with WordPress security best practices in mind. It does not make unauthorized changes or expose your site to external threats. All operations happen within your existing WordPress environment, and Angie only acts when you instruct it to. In fact, before performing any potentially destructive actions, like deleting content or overriding settings, Angie will alert you and ask for confirmation to ensure nothing happens unintentionally.
    142 
    143 = 8. Does Angie keep my information safe? =
    144 
    145 Absolutely. Angie does not collect or store sensitive site data outside of your WordPress installation. Any processing involving your site's content or structure is handled securely and in compliance with standard data privacy protocols.
    146 
    147 = 9. Can I connect my own MCP server to Angie? =
    148 
    149 Yes. Angie is fully extensible and allows plugin developers to integrate custom capabilities through the MCP (Model Context Protocol) system. Using the official Angie MCP SDK, you can build and register your own MCP server directly in the browser, no need for an external gateway or PHP-based server. This lets Angie discover and interact with your plugin's tools, resources, and workflows in real time. Whether you're exposing backend functionality or building advanced client-side features, Angie can run your MCP server and invoke your tools securely, right from the current screen.
    150 
    151 = 10. Is Angie in Beta? =
    152 
    153 Yes, Angie is in Beta, which means you'll get early access to powerful features and many more integrations with other WordPress plugins are on the way. Some actions may not be perfect yet as we continue to refine and improve the experience. Sharing your experience helps us make Angie better every day.
    154 
    155 == Screenshots ==
    156 
    157 1. Use Angie to jumpstart any project in WordPress. Write content, design sections, and manage tasks without leaving WordPress.
    158 2. Prompt Angie to plan and design pages in seconds, with Angie helping to add engaging content, media, and layouts.
    159 3. Edit existing images, create variations, or generate entirely new visuals, all within Angie. No third-party tools required.
    160 4. Ask Angie to run instant store-wide promotions, update prices, and refresh product listings automatically.
    161 
    162 ## Contributors & Developers
    163 
    164 "Angie: Agentic AI for WordPress" is open source software. The following people have contributed to this plugin.
    165 
     62= 1. What is Angie? =
     63
     64Angie is an AI layer for WordPress. Unlike standalone AI tools, Angie is purpose-built for the WordPress ecosystem with deep contextual understanding of your specific website, its architecture, installed plugins, and data. Angie acts as an AI-powered expert WordPress developer and agentic assistant designed to help you build and manage websites without writing any code. It functions like a senior engineer that turns plain-language descriptions into production-ready code instantly.
     65
     66= 2. What is Angie Code? =
     67
     68Angie Code is your WordPress developer on demand. Through natural conversation, you can create custom Elementor widgets, code snippets, and functionality, without requiring any code. Describe what you want, refine it through conversation, and Angie builds it for you. Every creation is fully editable and works seamlessly with your existing WordPress setup.
     69
     70Here's what you can create:
     71
     72- **Elementor widgets** - Create custom widgets from scratch and refine their style using native Elementor Editor controls.
     73- **Extend Elementor widgets** - Inject custom controls and advanced capabilities into existing widgets to expand their functional range.
     74- **Customize WordPress admin & backend** - Tailor your dashboard, add custom post types, extend WooCommerce, and implement custom hooks and filters.
     75- **Architect front-end enhancements** - Integrate CSS and JavaScript for sophisticated animations, cursor effects, and immersive interactive behaviors.
     76- **Construct visual apps** - Build fully functional front-end applications like custom calculators, and quizzes.
     77- **Generate complete landing pages** - Produce entire pages with layout, content, and styling for Elementor and Gutenberg.
     78
     79= 3. Is Angie Code compatible with Elementor? =
     80
     81Yes! Angie is deeply integrated with the Elementor Editor. You can generate custom widgets specifically for Elementor, allowing you to create unique layouts and functionalities that aren't available in standard widget packs. The widgets Angie creates work seamlessly with Elementor's visual controls, giving you the best of both worlds: AI-powered creation and manual refinement.
     82
     83= 4. Is it free to use? =
     84
     85Yes. Angie has a free tier with daily renewing credits, allowing you to explore its creation capabilities every day at no cost. This lets you experiment, learn, and build real projects without any financial commitment.
     86
     87= 5. How does "Test Mode" protect my site? =
     88
     89Test Mode is a secure staging environment where AI-generated code runs in isolation. You can preview, test, and interact with your creation without affecting your live site. Only when you're completely satisfied do you manually publish it to production.
     90
     91This ensures safety and control over every AI-generated element. You can:
     92
     93- See exactly what code will be added
     94- Turn widgets on and off to test behavior
     95- Review what's in production versus staging
     96- Make iterative changes in a safe space
     97- Publish to live with confidence
     98
     99No artifacts are published or affect your website until you've explicitly approved it.
     100
     101= 6. Can I use screenshots or URLs to build widgets? =
     102
     103Absolutely. Angie Code supports multiple input types:
     104
     105- **Text prompts** - Describe what you want in natural language
     106- **Screenshots** - Upload an image of a design you want to recreate
     107- **URLs** - Provide a website link and Angie analyzes the design
     108
     109Angie analyzes the layout, functionality, and design patterns to create a similar, fully editable artifacts for your site. This makes reverse-engineering inspiring designs incredibly fast.
     110
     111= 7. Do I need to be a developer to use Angie Code? =
     112
     113Not at all. Angie is designed to break the technical ceiling through conversation. Describe what you want, then refine it through follow-up prompts, just like working with a developer.
     114
     115"Make it more colorful," "Add animation," "Match my brand colors," Angie iterates with you until it's perfect. You don't need to nail the prompt on the first try. The power is in the conversation, not the single perfect instruction.
     116
     117The code remains open and fully editable for those who want to customize it manually. You're never locked in.
     118
     119= 8. Can I edit what Angie generates? =
     120
     121Yes. This is core to the Angie philosophy. Every widget and snippet Angie creates is fully editable, both through continued conversation with Angie and through manual code editing.
     122
     123You can:
     124
     125- Ask Angie to make changes conversationally
     126- Switch to Elementor's visual editor for pixel-perfect adjustments
     127- Edit the underlying PHP, CSS, and JavaScript directly
     128- Mix AI creation with manual refinement
     129
     130Your creations are never locked or proprietary. They're code that works with your entire ecosystem.
     131
     132= 9. Is it compatible with my theme and plugins? =
     133
     134Yes. Angie works across your entire WordPress environment. While it has deep integration with Elementor, Angie Code generates PHP, CSS, and JavaScript. Angie also understands your site's existing architecture and adapts automatically. Instead of replacing your tools, Angie is also an MCP that connects your ecosystem to help you get more from it.
     135
     136= 10. How does Angie handle site security? =
     137
     138Angie follows WordPress security best practices. It does not perform destructive actions (like deleting content or overriding core settings) without your explicit confirmation. Because Angie is context-aware, it understands your site's boundaries and works within WordPress's security model.
     139
     140Additionally:
     141
     142- All code runs in Test Mode before going live
     143- You manually approve every change to production
     144- Code is generated using WordPress coding standards
     145
     146= 11. Can I connect my own MCP server? =
     147
     148Yes. Angie is fully extensible. Developers can use Angie's official SDK on GitHub to integrate custom capabilities through the Model Context Protocol (MCP), allowing Angie to interact with your specific tools and workflows in real-time.
     149
     150Resources:
     151
     152- GitHub SDK
     153- NPM package
     154- Demo plugin
     155
     156= 12. Do I need to speak English to use Angie? =
     157
     158Not at all. Angie is built for creators worldwide. You can prompt Angie in your native language whether you speak Spanish, French, Japanese, or any other major language. Angie understands your intent and generates the correct content accordingly.
     159
     160Language should never be a barrier to bringing your WordPress ideas to life.
    166161
    167162== Changelog ==
     163
     164= 1.1.0 - 2026-02-19 =
     165* New: Angie Code: build custom Elementor widgets, Gutenberg blocks, code snippets, custom functionality, and interactive apps through conversation
     166* New: Preview environment for all code creations - test before going live
     167* New: Visual reference support - upload screenshots or URLs to generate matching components
     168* New: Custom post type and field generation through conversation
     169* Tweak: Added empty state and "Generate" button to Angie category
     170* Tweak: Added referrer redirect support
     171* Tweak: Removed wrapper overlay from WordPress interface when Angie is open
     172* Tweak: Removed unused Elementor One connection button
     173
    168174= 1.0.4 - 2026-01-20 =
    169175* Tweak: Optimized token fetching and handling in Angie plugin
     
    171177
    172178= 1.0.3 - 2026-01-12 =
    173 * Tweak: Added support for “Elementor One” connection
     179* Tweak: Added support for "Elementor One" connection
    174180* Tweak: Added Canvas page template to support Landing page creation in Gutenberg
    175181* Tweak: Added `animate.css` script for Gutenberg generated landing pages
Note: See TracChangeset for help on using the changeset viewer.