Salute Your Cursor AI Overlords

I’m not sure yet, but I’m pretty close to sure that AI can’t do anything genuinely complicated when it comes to writing code. Or, more accurately, it’s happy to do immensely complicated code creation, but most of the complex stuff doesn’t work and is more trouble than it’s worth to fix.

But a recent return to experimentation with ChatGPT impressed me enough that I think, within a narrow context and with requests that mimic other code in the codebase, AI should be able to handle requests like “change the blah field verification routine to add a check of whether blah.count > 5 and if it is, then return fail”.

My being convinced won’t buy you very much at the programmer store, but I’m nevertheless convinced. What’s more, I think there are huge tracts of PeakZebra that, with some careful refactoring, can be structured so that AI makes reliably accurate changes on request.

I’m saying ‘AI’ and not ChatGPT or some other LLM model/service because I simply don’t know enough just this moment to say which AI is mostly likely to do what I’m after.

Cursor buzz

That said, Cursor is the current candidate. There’s a positive buzz out there on the pipes but, far more important than that, Cursor looks at your codebase as it formulates its answers. My hope is that this will enable it to handle “basically the same thing that we did over here” type requests.

I’d love it if I could set up some rules for determining which kind of solution is best, but I simply don’t know nearly enough to know what that would look like, if any generally available LLC out there also supports traditional rule-guided expert system elements, and whether the real answer is to build a custom, local machine learning system to do the coding instead (unlikely).

To take one example of the sort of thing I’d like to be able to do, remembering that I’m doing this in the context of WordPress installations, I’d like to be able to say “add a hook to the blah action, write a callback function for it that follows my function naming conventions, have the function do blah, and then store this function in the client.php file.

In an ideal world, you’d then ask the AI to write a function that tests the new function and can be added to existing test regimes. No particular reason, with the correct (but perhaps not yet existent) connectivity, it couldn’t then run the test against the procedure to make sure it works as expected.

To me it sounds a little “out there” when I consider my wishlist as a whole, but then again, aren’t the various pieces of all these functions more or less already out there? Especially for use in a fairly well declared framework like WordPress?

Cursory beginnings

So I downloaded Cursor and am giving it a spin to see how much it can do for real world programming before it breaks up on the rocks of code complexity. I’m midway in the free trial period, and things are looking extremely promising with it as a way to let the magic box product a lot of the code-talkin’.

Downloading Cursor was something I recently threatened to do, so if nothing else I’m leveling up on blog promise follow through. I won’t spend time describing Cursor, because odds are good you’ve already heard something about it. In any case, lots of other great general writeups are already out there.

WordPress Knowledge?

My big first question was whether it would be accurate in its understanding (?!) of the WordPress context. So I opened up the plugins directory on a local instance where I’ve been working. I can’t really say how well it has a grasp on the relationship of the several plugins I have in there, but it’s definitely looking at all the code in the directory.

While context is king in AI, it occurred to me that even on a dev installation, you may well have several third-party plugins installed. Best not to waste context on what could amount to a whole lot of code that has nothing to do with your dev efforts. So, probably better where Cursor is concerned to mash everything into one plugin and open that single plugin’s top folder, rather than the plugins folder itself.

Better yet, perhaps one can simply move the third-party stuff to the MU (must use) plugins directory. I’ll give that a spin and report back.

Working in Ignorance

I’ll confess that I haven’t yet taking the time to really learn what’s going on in the Cursor interface. Even so, I figured out how to open a chat window alongside a file I was working on and have a chat that was aware of the open file(s) I was working on. (This isn’t actually the best way to generate code within existing files and codebases.)

It seems to work pretty darned well to have a specific thing in mind that can be packaged as a function. So you can prompt things like “write a function that takes the task and the rate, consults a table for the standard number of hours that should be estimated for that task and returns the estimated revenue for the task. It seems generally able to figure out things like “task and rate” will need to be variables. It even seems to put together a lot of things like “rate times hours” is what the answer is. There’s a fair likelihood that it will understand that, absent any further context, it will need to create a dummy table with some sample tasks and hours and it will give you a vaguely pedantic explanation in its response that says all you need to do is replace the dummy values with your real ones.

For example

I wanted some help text that appeared on relevant pages and sections in PeakZebra and I wanted each user to decide when the help text boxes could be dismissed and not be shown anymore. Plus I wanted any individual user decide they needed the help again and be able to toggle the help boxes back on.

I did this in a few discrete steps. First, I asked for a function that would add a flag variable called is_help to each user’s metadata. Then I requested code to add the field to each user’s profile page. Cursor did this with no issues.

I asked it to create a block for my help text:

It gave me a two-file setup that looked pretty good but didn’t quite work:

help-text-block.php

<?php
/**
 * Plugin Name: Help Text Block
 * Description: A block that displays text only to users with is_help meta field set to true
 * Version: 1.0.0
 * Author: Your Name
 */

if (!defined('ABSPATH')) {
    exit;
}

function help_text_block_register() {
    wp_register_script(
        'help-text-block-editor',
        plugins_url('block.js', __FILE__),
        array('wp-blocks', 'wp-element', 'wp-editor')
    );

    register_block_type('help-text/conditional-text', array(
        'editor_script' => 'help-text-block-editor',
        'render_callback' => 'help_text_block_render'
    ));
}
add_action('init', 'help_text_block_register');

function help_text_block_render($attributes, $content) {
    // Get current user
    $current_user = wp_get_current_user();
    if (!$current_user) {
        return '';
    }

    // Check if user has is_help meta set to true
    $is_help = get_user_meta($current_user->ID, 'is_help', true);
    
    // Only show content if is_help is true
    if ($is_help) {
        return $content;
    }
    
    return '';
}

and block.js

(function(blocks, element) {
    var el = element.createElement;

    blocks.registerBlockType('help-text/conditional-text', {
        title: 'Help Text',
        icon: 'text',
        category: 'common',
        attributes: {
            content: {
                type: 'string',
                source: 'html',
                selector: 'p',
            }
        },
        
        edit: function(props) {
            return el(
                wp.blockEditor.RichText,
                {
                    tagName: 'p',
                    className: props.className,
                    value: props.attributes.content,
                    onChange: function(newContent) {
                        props.setAttributes({ content: newContent });
                    },
                    placeholder: 'Enter help text here...'
                }
            );
        },

        save: function(props) {
            return el(
                wp.blockEditor.RichText.Content,
                {
                    tagName: 'p',
                    value: props.attributes.content
                }
            );
        }
    });
}(
    window.wp.blocks,
    window.wp.element
));

I’ll use subsequent posts to talk more about what does and doesn’t work with Cursor, but I want to highlight something here and right off the bat: this is arguably the “old school” way of creating a block. These days, you’d expect most of the block to be declared in a separate block.json file. And you’d expect a javascript file that needed to be run through npm start to generate a finished version in a build directory.

Does it dream of electric sheep?

And here’s the weird thing about working with something like Cursor (and the various LLM models it uses in turn): it’s possible that this is a savvy choice. After all, it avoids the whole process of processing. It’s ready to go as soon as it turns up in the plugins directory.

One argument in favor of seeing it as savvy is that it correctly wrote the JavaScript file in vanilla script. That’s why it calls createElement right out of the gate and why it looks weirdly overcomplicated.

On the other hand, maybe it’s just modeling outdated stuff on the internet. There have been lots of changes in the “typical” block build over the past year and thus there are lots of bits of sample code out there that do things in ways that work but aren’t really what developers are currently doing.

I was thinking it was possibly picking up older blocks in my code base, because some of them still have earlier techniques in them. But it occurred to me that I’ve never not used webpack in my block building, so presumably not.

In any case, you can’t tell what it was thinking or where it was getting its thinking from, as far as I’m aware. I think chat context is probably the way to help control where the magic gets its smarts, but I have a lot to learn on that front.

I’ll discuss actual code quality (as well as having Cursor debug its own code) in the next couple of posts.