settingsService = $settingsService; $this->providerInfo = $providerInfo; // NCF page and field registration. add_filter('native_custom_fields_options_pages', [$this, 'registerSettingsPage']); add_filter('native_custom_fields_options_page_fields', [$this, 'registerSettingsFields'], 10, 2); // Markdown editor for the prompt textareas on the settings page. add_action('admin_enqueue_scripts', [$this, 'enqueueMarkdownEditor']); } /** * Registers (always) and enqueues (settings page only) the markdown * editor bundle. * * The bundle enhances long-form textareas that carry an "agent-mod-md-" * id with an EasyMDE markdown editor. It is registered on every admin * page so that extensions (e.g. PRO's agent/skill edit screens, which tag * their own NCF fields with the same id prefix) can enqueue the * `agent-mod-settings-editor` script/style handles themselves; it is * enqueued here only on the NCF settings page, including when that page * is embedded in the PRO workspace iframe. * * @return void * @since 1.0.7 */ public function enqueueMarkdownEditor(): void { if (! current_user_can('manage_options')) { return; } $asset_path = AGENT_MOD_PATH . 'build/settings-editor/index.asset.php'; // Silently bail if the bundle has not been built yet. if (! is_readable($asset_path)) { return; } $asset = require $asset_path; wp_register_script( 'agent-mod-settings-editor', AGENT_MOD_URL . 'build/settings-editor/index.js', array_merge($asset['dependencies'], ['jquery']), $asset['version'], true ); wp_register_style( 'agent-mod-settings-editor', AGENT_MOD_URL . 'build/settings-editor/style-index.css', ['dashicons'], $asset['version'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only URL parameter check to determine current admin page; no form data is processed. $current_page = isset($_GET['page']) ? sanitize_key(wp_unslash($_GET['page'])) : ''; if ($this->settingsService->optionKey !== $current_page) { return; } wp_enqueue_script('agent-mod-settings-editor'); wp_enqueue_style('agent-mod-settings-editor'); } /** * Registers the AgentMod settings page as a submenu of the main AgentMod menu. * * @param array $options_pages Existing NCF page configurations. * * @return array * @since 1.0.0 */ public function registerSettingsPage(array $options_pages): array { $options_pages[$this->settingsService->optionKey] = [ 'page_title' => __('AgentMod Settings', 'agent-mod'), 'menu_title' => __('Settings', 'agent-mod'), 'menu_slug' => $this->settingsService->optionKey, 'capability' => 'manage_options', 'layout' => 'tab_panel', 'parent_slug' => 'agent-mod', ]; return $options_pages; } /** * Registers field sections and fields for the AgentMod settings page. * * @param array $sections Existing sections (from other pages or filters). * @param string $menu_slug The menu slug of the page being built. * * @return array * @since 1.0.0 */ public function registerSettingsFields(array $sections, string $menu_slug): array { if ($this->settingsService->optionKey !== $menu_slug) { return $sections; } return [ [ 'section_name' => 'agent_mod_chat_behaviour', 'section_title' => __('Chat Behaviour', 'agent-mod'), 'section_icon' => 'format-chat', 'fields' => [ [ 'fieldType' => 'token_field', 'name' => 'personality_traits', 'fieldLabel' => __('Personality Traits', 'agent-mod'), 'fieldHelpText' => __('Add personality traits.', 'agent-mod'), 'maxSuggestions' => 10, 'suggestions' => Helper::personalityTraitOptions(), '__experimentalExpandOnFocus' => true, ], [ 'fieldType' => 'toggle', 'name' => 'site_context_enabled', 'fieldLabel' => __('Enable Site Context (RAG) by Default', 'agent-mod'), 'fieldHelpText' => __('When enabled, the agent automatically includes site metadata (name, URL, WordPress version, etc.) in its system prompt.', 'agent-mod'), 'default' => Constants::AI_CONTEXT_ENABLED, ], [ 'fieldType' => 'toggle', 'name' => 'web_search_enabled', 'fieldLabel' => __('Enable Web Search', 'agent-mod'), 'fieldHelpText' => __('When enabled, the agent can search the live web to answer questions. Uses the AI provider\'s native web search (OpenAI web_search / Google googleSearch). Availability depends on the selected model.', 'agent-mod'), 'default' => Constants::AI_WEB_SEARCH_ENABLED, ], [ 'fieldType' => 'textarea', 'name' => 'role', 'id' => 'agent-mod-md-role', 'fieldLabel' => __('Role', 'agent-mod'), 'fieldHelpText' => __('The role the agent should play. Describe the main function of the agent.', 'agent-mod'), 'default' => Constants::AI_AGENT_DEFAULT_ROLE ], [ 'fieldType' => 'textarea', 'name' => 'goal', 'id' => 'agent-mod-md-goal', 'fieldLabel' => __('Goal', 'agent-mod'), 'fieldHelpText' => __('The goal of the agent. Describe what you want the agent to achieve.', 'agent-mod'), 'default' => Constants::AI_AGENT_DEFAULT_GOAL ], [ 'fieldType' => 'textarea', 'name' => 'global_system_prompt', 'id' => 'agent-mod-md-global-system-prompt', 'fieldLabel' => __('Base System Prompt', 'agent-mod'), 'fieldHelpText' => __('The core behaviour instructions sent to the AI with every message. Fully editable: change or remove directives to manage the assistant\'s behaviour. Leave empty to use the built-in defaults.', 'agent-mod'), 'placeholder' => __('You are a helpful WordPress assistant.', 'agent-mod'), 'default' => Constants::aiDefaultSystemPrompt(), ], ], ], [ 'section_name' => 'agent_mod_ai_model', 'section_title' => __('AI Model', 'agent-mod'), 'section_icon' => 'admin-generic', 'fields' => [ [ 'fieldType' => 'combobox', 'name' => 'default_model', 'fieldLabel' => __('Default AI Model', 'agent-mod'), 'fieldHelpText' => __('Pre-selected automatically whenever a new chat is started. Picking a model also picks its provider. Leave "Use Auto" to let the AI Client choose.', 'agent-mod'), 'options' => $this->getModelOptions(), 'default' => '', ], ], ], [ 'section_name' => 'agent_mod_chat_panel', 'section_title' => __('Chat Panel', 'agent-mod'), 'section_icon' => 'admin-comments', 'fields' => [ [ 'fieldType' => 'repeater', 'name' => 'preset_prompts', 'fieldLabel' => __('Preset Prompts', 'agent-mod'), 'fieldHelpText' => __('Quick-start prompts shown above the chat input. Clicking one fills the message box with the prompt text.', 'agent-mod'), 'layout' => 'table', 'addButtonText' => __('Add Prompt', 'agent-mod'), 'min' => 0, 'max' => 10, 'fields' => [ [ 'fieldType' => 'text', 'name' => 'label', 'fieldLabel' => __('Label', 'agent-mod'), ], [ 'fieldType' => 'textarea', 'name' => 'prompt', 'fieldLabel' => __('Prompt', 'agent-mod'), ], ], 'default' => Constants::aiDefaultPresetPrompts() ], ], ], [ 'section_name' => 'agent_mod_abilities', 'section_title' => __('Abilities', 'agent-mod'), 'section_icon' => 'admin-plugins', 'fields' => [ [ 'fieldType' => 'select', 'name' => 'tool_loading', 'fieldLabel' => __('Tool Loading', 'agent-mod'), 'fieldHelpText' => __('Dynamic (recommended): the agent starts with a few tools and loads the abilities it needs by searching for them, which keeps every request small. All: every ability is sent with every request; costs far more tokens on sites with many abilities.', 'agent-mod'), 'options' => [ [ 'label' => __('Dynamic (recommended)', 'agent-mod'), 'value' => 'dynamic', ], [ 'label' => __('All abilities', 'agent-mod'), 'value' => 'all', ], ], 'default' => Constants::AI_TOOL_LOADING, ], ], ], [ 'section_name' => 'agent_mod_ai_limits', 'section_title' => __('AI Limits', 'agent-mod'), 'section_icon' => 'performance', 'fields' => [ [ 'fieldType' => 'number', 'name' => 'max_tool_calls', 'fieldLabel' => __('Max Tool Calls per Turn', 'agent-mod'), 'fieldHelpText' => __('Maximum agentic tool-calling iterations allowed per chat message. Default: 10.', 'agent-mod'), 'default' => Constants::AI_MAX_TOOL_CALLS, ], [ 'fieldType' => 'number', 'name' => 'max_search_results', 'fieldLabel' => __('Max Search Results', 'agent-mod'), 'fieldHelpText' => __('Maximum items returned per search ability call. Default: 20.', 'agent-mod'), 'default' => Constants::AI_MAX_SEARCH_RESULTS, ], [ 'fieldType' => 'number', 'name' => 'max_full_content_posts', 'fieldLabel' => __('Max Full-Content Posts', 'agent-mod'), 'fieldHelpText' => __('Maximum posts whose full body the agent may read in a single turn. Default: 5.', 'agent-mod'), 'default' => Constants::AI_MAX_FULL_CONTENT_POSTS, ], [ 'fieldType' => 'number', 'name' => 'max_answer_length', 'fieldLabel' => __('Max Answer Length (characters)', 'agent-mod'), 'fieldHelpText' => __('Maximum length of a free-text answer the user types when the agent asks questions or requests approval. Longer answers are cut off. Default: 20000.', 'agent-mod'), 'default' => Constants::AI_MAX_ANSWER_LENGTH, ], ], ], [ 'section_name' => 'agent_mod_attachments', 'section_title' => __('File Attachments', 'agent-mod'), 'section_icon' => 'paperclip', 'fields' => [ [ 'fieldType' => 'number', 'name' => 'attachment_max_count', 'fieldLabel' => __('Max Files per Message', 'agent-mod'), 'fieldHelpText' => __('Maximum number of files a user may attach per chat turn. Default: 5.', 'agent-mod'), 'default' => Constants::AI_ATTACHMENT_MAX_COUNT, ], [ 'fieldType' => 'number', 'name' => 'attachment_max_bytes', 'fieldLabel' => __('Max File Size (bytes)', 'agent-mod'), 'fieldHelpText' => __('Maximum decoded size in bytes for a single attachment. Default: 5242880 (5 MB).', 'agent-mod'), 'default' => Constants::AI_ATTACHMENT_MAX_BYTES, ], [ 'fieldType' => 'textarea', 'name' => 'attachment_mime_types', 'fieldLabel' => __('Allowed MIME Types', 'agent-mod'), 'fieldHelpText' => __('One MIME type per line. Leave empty to use the built-in defaults (PNG, JPEG, GIF, WebP, PDF, TXT, Markdown, CSV).', 'agent-mod'), 'default' => implode("\n", Constants::AI_ATTACHMENT_MIME_TYPES), ], ], ], ]; } /** * Get the default-model select options: "Use Auto" plus every text model * offered by the currently connected AI providers. * * A live fetch is safe here — registerSettingsFields() has already bailed * out for every screen except the AgentMod settings page, and the per * provider model lists are transient-cached for six hours afterwards. * * @return array> * @since 1.1.0 */ private function getModelOptions(): array { return array_merge( [ [ 'label' => __('Use Auto', 'agent-mod'), 'value' => '', ], ], $this->providerInfo->getModelOptions() ); } }