@@ -247,16 +393,20 @@ const SystemTasksTab = () => {
className="description"
style={ { marginTop: '16px', marginBottom: '16px' } }
>
- Registered task definitions. Each task can run standalone via
- Action Scheduler or as a step in a pipeline flow.
+ Registered task definitions. Each task can run via Action
+ Scheduler or as a step in a pipeline flow. Tasks with AI
+ prompts can be customized below.
{ tasks.map( ( task ) => (
diff --git a/inc/Core/Admin/Pages/Agent/assets/react/queries/systemTaskPrompts.js b/inc/Core/Admin/Pages/Agent/assets/react/queries/systemTaskPrompts.js
new file mode 100644
index 00000000..41542ef1
--- /dev/null
+++ b/inc/Core/Admin/Pages/Agent/assets/react/queries/systemTaskPrompts.js
@@ -0,0 +1,77 @@
+/**
+ * System Task Prompts Queries
+ *
+ * TanStack Query hooks for system task prompt CRUD via REST API.
+ *
+ * @since 0.43.0
+ */
+
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import { client } from '@shared/utils/api';
+
+const PROMPTS_KEY = [ 'system-task-prompts' ];
+
+/**
+ * Fetch all system task prompt definitions with overrides.
+ */
+export const useSystemTaskPrompts = () =>
+ useQuery( {
+ queryKey: PROMPTS_KEY,
+ queryFn: async () => {
+ const result = await client.get( '/system/tasks/prompts' );
+ if ( ! result.success ) {
+ throw new Error(
+ result.message || 'Failed to fetch task prompts'
+ );
+ }
+ return result.data;
+ },
+ staleTime: 60 * 1000,
+ } );
+
+/**
+ * Save a prompt override.
+ */
+export const useSavePrompt = () => {
+ const queryClient = useQueryClient();
+ return useMutation( {
+ mutationFn: async ( { taskType, promptKey, prompt } ) => {
+ const result = await client.put(
+ `/system/tasks/prompts/${ taskType }/${ promptKey }`,
+ { prompt }
+ );
+ if ( ! result.success ) {
+ throw new Error(
+ result.message || 'Failed to save prompt'
+ );
+ }
+ return result.data;
+ },
+ onSuccess: () => {
+ queryClient.invalidateQueries( { queryKey: PROMPTS_KEY } );
+ },
+ } );
+};
+
+/**
+ * Reset a prompt to its default.
+ */
+export const useResetPrompt = () => {
+ const queryClient = useQueryClient();
+ return useMutation( {
+ mutationFn: async ( { taskType, promptKey } ) => {
+ const result = await client.delete(
+ `/system/tasks/prompts/${ taskType }/${ promptKey }`
+ );
+ if ( ! result.success ) {
+ throw new Error(
+ result.message || 'Failed to reset prompt'
+ );
+ }
+ return result.data;
+ },
+ onSuccess: () => {
+ queryClient.invalidateQueries( { queryKey: PROMPTS_KEY } );
+ },
+ } );
+};
diff --git a/inc/Core/Admin/Pages/Pipelines/assets/react/components/pipelines/PipelineStepCard.jsx b/inc/Core/Admin/Pages/Pipelines/assets/react/components/pipelines/PipelineStepCard.jsx
index 2474994d..29f84c6f 100644
--- a/inc/Core/Admin/Pages/Pipelines/assets/react/components/pipelines/PipelineStepCard.jsx
+++ b/inc/Core/Admin/Pages/Pipelines/assets/react/components/pipelines/PipelineStepCard.jsx
@@ -13,7 +13,7 @@ import { __ } from '@wordpress/i18n';
/**
* Internal dependencies
*/
-import PromptField from '../shared/PromptField';
+import PromptField from '@shared/components/PromptField';
import { updateSystemPrompt } from '../../utils/api';
import { useStepTypes, useTools } from '../../queries/config';
diff --git a/inc/Core/Admin/shared/components/PromptField.jsx b/inc/Core/Admin/shared/components/PromptField.jsx
new file mode 100644
index 00000000..f6a68143
--- /dev/null
+++ b/inc/Core/Admin/shared/components/PromptField.jsx
@@ -0,0 +1,178 @@
+/**
+ * PromptField Component
+ *
+ * Reusable textarea field with auto-save (debounced), saving indicator,
+ * and error handling. Works for AI system prompts, flow step user messages,
+ * Agent Ping prompts, and any editable text field.
+ *
+ * @since 0.42.0
+ */
+
+/**
+ * WordPress dependencies
+ */
+import { useState, useEffect, useCallback, useRef } from '@wordpress/element';
+import { TextareaControl, Notice } from '@wordpress/components';
+import { __ } from '@wordpress/i18n';
+
+/**
+ * Auto-save delay in milliseconds.
+ */
+const AUTO_SAVE_DELAY = 500;
+
+/**
+ * PromptField Component
+ *
+ * @param {Object} props - Component props
+ * @param {string} props.value - Current field value
+ * @param {Function} props.onChange - Change handler (for immediate updates)
+ * @param {Function} props.onSave - Save handler (async, returns { success, message? })
+ * @param {string} props.label - Field label
+ * @param {string} props.placeholder - Placeholder text
+ * @param {number} props.rows - Number of textarea rows (default: 6)
+ * @param {string} props.help - Help text (overridden when saving)
+ * @param {boolean} props.disabled - Whether the field is disabled
+ * @param {string} props.className - Additional CSS class
+ * @return {React.ReactElement} Prompt field component
+ */
+export default function PromptField( {
+ value = '',
+ onChange,
+ onSave,
+ label,
+ placeholder,
+ rows = 6,
+ help,
+ disabled = false,
+ className = '',
+} ) {
+ const [ localValue, setLocalValue ] = useState( value );
+ const [ isSaving, setIsSaving ] = useState( false );
+ const [ error, setError ] = useState( null );
+ const saveTimeout = useRef( null );
+ const lastSavedValue = useRef( value );
+
+ /**
+ * Sync local value with external value changes
+ */
+ useEffect( () => {
+ setLocalValue( value );
+ lastSavedValue.current = value;
+ }, [ value ] );
+
+ /**
+ * Save value to API
+ */
+ const saveValue = useCallback(
+ async ( newValue ) => {
+ if ( ! onSave ) {
+ return;
+ }
+
+ // Skip if unchanged
+ if ( newValue === lastSavedValue.current ) {
+ return;
+ }
+
+ setIsSaving( true );
+ setError( null );
+
+ try {
+ const result = await onSave( newValue );
+
+ if ( result?.success === false ) {
+ setError(
+ result.message || __( 'Failed to save', 'data-machine' )
+ );
+ // Revert to last saved value on error
+ setLocalValue( lastSavedValue.current );
+ } else {
+ lastSavedValue.current = newValue;
+ }
+ } catch ( err ) {
+ // eslint-disable-next-line no-console
+ console.error( 'PromptField save error:', err );
+ setError(
+ err.message || __( 'An error occurred', 'data-machine' )
+ );
+ // Revert to last saved value on error
+ setLocalValue( lastSavedValue.current );
+ } finally {
+ setIsSaving( false );
+ }
+ },
+ [ onSave ]
+ );
+
+ /**
+ * Handle value change with debouncing
+ */
+ const handleChange = useCallback(
+ ( newValue ) => {
+ setLocalValue( newValue );
+
+ // Call immediate onChange if provided
+ if ( onChange ) {
+ onChange( newValue );
+ }
+
+ // Clear existing timeout
+ if ( saveTimeout.current ) {
+ clearTimeout( saveTimeout.current );
+ }
+
+ // Set new timeout for debounced save
+ if ( onSave ) {
+ saveTimeout.current = setTimeout( () => {
+ saveValue( newValue );
+ }, AUTO_SAVE_DELAY );
+ }
+ },
+ [ onChange, onSave, saveValue ]
+ );
+
+ /**
+ * Cleanup timeout on unmount
+ */
+ useEffect( () => {
+ return () => {
+ if ( saveTimeout.current ) {
+ clearTimeout( saveTimeout.current );
+ }
+ };
+ }, [] );
+
+ /**
+ * Get help text with saving indicator
+ */
+ const getHelpText = () => {
+ if ( isSaving ) {
+ return __( 'Saving…', 'data-machine' );
+ }
+ return help;
+ };
+
+ return (
+
+ { error && (
+ setError( null ) }
+ >
+ { error }
+
+ ) }
+
+
+
+ );
+}