I recently read this fantastic blog post which inspired me to build my very own AI Programming Assistant. The first thing I thought when reading the blog post was how simple it actually is to build an agent which underpins all this.
As someone who uses Zed, I use their Agent feature heavily in my day to day work which often feels like magic. Being able to offload much of the ‘grunt’ work to the agent is a huge time-saver and makes building software more enjoyable. I was pleasantly surprised to realise this isn’t simply magic!
In the blog post it provides an example written in Python. I thought it’d be interesting to write something similar in Ruby. For my agent I decided I would use RubyLLM but this could easily have been built without it.
One of the benefits of this library is it makes switching between different models easy. Rather than having to figure out the intricacies of each model provider’s API, everything is nicely abstracted with a simple RubyLLM.chat call. Making it ideal for experimenting with different models.
Loop + Tools = Agent
require 'ruby_llm'
require 'dotenv/load'
class EditFile < RubyLLM::Tool
description "Edit the contents of a file. Before editing, use the read file tool."
param :path, desc: "The path to the file"
param :new_content, desc: "The new content to write to the file"
def execute(path:, new_content:)
begin
File.write(path, new_content)
{ success: true, content: File.read(path) }
rescue => e
{ error: e.message }
end
end
end
class FindPath < RubyLLM::Tool
description "Glob search for paths by filename (supports `**/*.rb`, `src/**`, etc.).
Best when you know part of a path but not its exact location."
param :glob, desc: "The glob pattern to search for"
def execute(glob:)
begin
matches = Dir.glob(glob).map { |path| File.expand_path(path) }
{ success: true, matches: matches }
rescue => e
{ error: e.message }
end
end
end
class ReadFile < RubyLLM::Tool
description "Read the contents of a file."
param :path, desc: "The path to the file"
def execute(path:)
begin
{ success: true, content: File.read(path) }
rescue => e
{ error: e.message }
end
end
end
RubyLLM.configure do |config|
config.openai_api_key = ENV['OPENAI_API_KEY']
end
TOOLSET = [
FindPath,
ReadFile,
EditFile
]
chat = RubyLLM.chat(model: "gpt-4.1")
chat.with_tools(*TOOLSET)
while true
puts "You:"
command = gets.chomp
response = chat.ask(command)
puts "Agent: #{response.content}"
end
A Working Agent
In my example I wrote three tools. FindPath, ReadFile and EditFile. The main ‘agent’ is simply a while loop. Inside this I collect user input which is passed to the LLM and from there it provides a response.
The model has context of the available tools, so it can decide to use them if it thinks it’s necessary. Executing a tool call is again nicely handled by RubyLLM.
So there you have it. By combining three simple tools, a loop and an LLM - I can instruct an agent from my terminal to make changes directly to my code. Pretty cool if you ask me!
I’ve been wanting to write more on this blog but it’s always been a hassle to get anything published. Mainly because this site is a rails app and by default it’s not great for managing and publishing content.
It’s much easier and quicker to use something like HEY or Pagecord. Writing, editing and publishing is frictionless! Exactly what you need when trying to create a new habit.
Despite this, it’s fairly trivial to close the gap between your rails app and the aforementioned blogging tools. Plus, there’s something appealing about having no limits on the customizations you can undertake.
Want to use your favourite text editor theme to render code blocks? Write using markdown? File based content managment? Use a web based editor?
You can get all this easily in rails with some well chosen libraries and gems and IMO is worth the extra effort. Give it a go!
File based content manager. Think frontmatter, human readable slugs, layouts (create a blog post layout and render markdown inside 😍) and loads more great features which make managing content easy. If you used Jekyll you’ll feel right at home. For a good overview of Sitepress, watch this video.
Works hand in hand with Sitepress. Let’s you create *.html.md files so you can write in markdown and it’ll all render out of the box without any custom config, but it’s worth extending ApplicationMarkdown to customize this.
I recently implemented modals on a side project and thought I’d share how I did it. They were surprisingly straightforward to implement when leveraging Turbo Frames, Tailwind CSS, and very minimal JavaScript.
If you prefer rawdogging it with pure CSS then feel free to drop Tailwind. My preference is Tailwind due to how much more productive I find working from the same file. Plus, I discovered a neat way to use a modern CSS selector in Tailwind by creating my own custom variant. More on this later.
For this blog post I’ve made available the source repo here and a live demo here. I’m also assuming at least basic understanding of Ruby on Rails.
To demonstrate the modals we’ll create a simple blog. We’ll use a Post model with title and content attributes and we’ll create a PostsController with all the typical CRUD actions you’d expect (check the source repo if you’re not sure about this part).
Editing a post inside a modal
For our example we will add an ‘edit’ link to our post’s show page. When the ‘edit’ link is clicked a modal will appear and the post’s edit form will be injected inside the modal, allowing the post to be edited from the show page.
The first step is to create a modal partial which we can reuse throughout our application. In our modal we’ll use a <dialog> element which is perfect for creating modals. We’ll also include a Turbo Frame within our modal and give it an ID of modal.
For now we’ll add Tailwind’s hidden class to make sure this modal is hidden by default. There’s also a bunch of other classes I’ve added for positioning and blurring the background content. Adjust as you see fit.
Finally, to make this available for use throughout our application we need to render this in our application.html.erb.
After creating the modal we need to amend or create our existing edit.html.erb template. See the code below—it’s a standard edit page with a form for capturing user data. You may have noticed a turbo frame tag with the same ID of modal. This is important to note because when it comes to injecting content into our modal, the turbo frames need to have matching IDs. More on this later…
Next we’ll add a link and when clicked it will open our modal and inject the edit form within it.
Typically when a user clicks a link to our edit page it does a HTTP request to the server, the server processes the request and then it returns a complete HTML page for the browser to render.
With the modals however, we want to ‘hijack’ this typical HTTP request. We do so by adding a data-turbo-frame: 'modal' attribute to the link, as shown below.
Adding the attribute allows Turbo to intercept the link click and instead tells Turbo to find a matching turbo frame in the DOM with an ID of “modal”.
Turbo will then perform an AJAX request to the edit_post_path and it’ll return our edit.html.erb template in the response, however Turbo is clever enough to only use the content inside our Turbo Frame (remember we gave it a matching ‘modal’ ID).
It then takes the edit page frame content and inserts it into the modal’s turbo frame. Not bad!
There’s still one problem: our modals are hidden! So even though the content has been injected it’s not visible to the user.
Showing our Modal
Most people would reach for JavaScript at this point which is totally fine. However, rather than adding or removing classes using JavaScript we can essentially have a CSS rule which says if this frame has content, make it visible, else it should be hidden.
To accomplish this we can use a little bit of CSS in the form of the relatively new :has() selector. The solution I am going to show uses a Tailwind custom variant. If you prefer pure CSS don’t worry, you will get the gist of it.
Tailwind custom variants are something which I have only just discovered. They essentially allow you to extend Tailwind with your own utility classes which come in handy for situations like this.
In our tailwind.config.js, we want add the below variant. If you’re familiar with Tailwind, you’ll be aware it lets you do things like this hover:cursor-pointer, i.e. when an element is hovered over it will change the cursor to be a pointer. It’s what makes Tailwind so powerful and productive.
We can do something similar with our own variant. We’ll create a variant called has-turbo-frame-content. When used on an element (our modal) it can check if the element has a turbo frame within it AND the frame is not empty. If it’s not empty then we want to make the modal visible by changing it from hidden (display: none) to visible (display: block).
Excellent. If you made it this far you can now successfully click the edit link and the modal will appear with the edit form successfully injected inside of it.
Sprinkles of JavaScript
To round this off, let’s add a close button and the ability to click outside the modal to close it. For this we do need a sprinkle of JavaScript.
You’ll notice in the stimulus controller all we need to do is clear the inner HTML and our use of the :has() selector will detect there is no content, thus it will revert to the default state of hidden!
Finally, here is the final modal partial with the close button and stimulus data attributes added.
If you made it this far I hope you found this blog post useful. I really do like this modal implementation due to the amount of flexibility it provides and thanks to the Hotwire libraries it really doesn’t take an awful lot of effort either.
Don’t forget to check out the source repo and live demo if you want to play around with this yourself.