Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
"scripts": {
"dev": "pnpm --filter agentation watch & pnpm --filter feedback-tool-example dev",
"build": "pnpm --filter agentation build",
"build:rails": "pnpm --filter agentation build && pnpm --filter agentation-rails-build build",
"build:all": "pnpm build && pnpm --filter agentation-rails-build build",
"example": "pnpm --filter feedback-tool-example dev",
"pack": "cd package && pnpm pack",
"mcp": "pnpm --filter agentation-mcp start",
Expand All @@ -17,5 +19,6 @@
"better-sqlite3",
"esbuild"
]
}
},
"packageManager": "pnpm@10.13.1+sha512.37ebf1a5c7a30d5fabe0c5df44ee8da4c965ca0c5af3dbab28c3a1681b70a256218d05c81c9c0dcf767ef6b8551eb5b960042b9ed4300c59242336377e01cfad"
}
34 changes: 34 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ packages:
- 'package'
- 'package/example'
- 'mcp'
- 'rails'
1 change: 1 addition & 0 deletions rails/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules/
27 changes: 27 additions & 0 deletions rails/gem/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
PolyForm Shield License 1.0.0

Copyright (c) 2026 Benji Taylor

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to use,
copy, modify, and distribute the Software, subject to the following conditions:

1. You may not use the Software to provide a product or service that competes
with the Software or any product or service offered by the Licensor that
includes the Software.

2. You may not remove or obscure any licensing, copyright, or other notices
included in the Software.

3. If you distribute the Software or any derivative works, you must include a
copy of this license.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

For more information, see https://polyformproject.org/licenses/shield/1.0.0
87 changes: 87 additions & 0 deletions rails/gem/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# agentation-rails

Drop-in Rails engine that adds the [Agentation](https://github.com/benjitaylor/agentation) annotation toolbar to your app in development. One line in your Gemfile, zero configuration.

## Installation

```ruby
# Gemfile
gem "agentation-rails", group: :development
```

```bash
bundle install
```

The toolbar appears automatically in development. Nothing to configure.

## Configuration (optional)

Generate an initializer:

```bash
rails generate agentation:install
```

Or add one manually:

```ruby
# config/environments/development.rb
Agentation.configure do |config|
config.endpoint = "http://localhost:4747" # MCP sync server (default)
config.webhook_url = "https://example.com/hooks/agentation"
config.copy_to_clipboard = false # disable auto-copy
end
```

## JavaScript events

The toolbar dispatches `CustomEvent`s on `document` for every annotation lifecycle event. Use these with Stimulus controllers or plain JS:

```javascript
document.addEventListener("agentation:add", (e) => {
console.log("Annotation added:", e.detail);
});

document.addEventListener("agentation:delete", (e) => {
console.log("Annotation deleted:", e.detail);
});

document.addEventListener("agentation:update", (e) => {
console.log("Annotation updated:", e.detail);
});

document.addEventListener("agentation:clear", (e) => {
console.log("Annotations cleared:", e.detail);
});

document.addEventListener("agentation:copy", (e) => {
console.log("Copied markdown:", e.detail.markdown);
});

document.addEventListener("agentation:submit", (e) => {
console.log("Submitted:", e.detail.output, e.detail.annotations);
});

document.addEventListener("agentation:session", (e) => {
console.log("Session created:", e.detail.sessionId);
});
```

## How it works

The gem inserts a `<script>` tag into every HTML response in development via Rack middleware. The script loads the Agentation toolbar, which lets you annotate page elements and send structured feedback to AI coding agents via the [Agentation MCP server](https://www.npmjs.com/package/agentation-mcp).

## Development

The built JS artifact is generated from the monorepo root:

```bash
pnpm build:rails
```

This bundles React + Agentation into a single IIFE at `gem/app/assets/javascripts/agentation.js`.

## License

PolyForm Shield 1.0.0
23 changes: 23 additions & 0 deletions rails/gem/agentation-rails.gemspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
Gem::Specification.new do |spec|
spec.name = "agentation-rails"
spec.version = "2.1.1"
spec.authors = ["Benji Taylor"]
spec.summary = "Visual annotation toolbar for AI coding agents in Rails"
spec.description = "Drop-in Rails engine that adds the Agentation toolbar to your app in development. " \
"One line in your Gemfile, zero configuration."
spec.homepage = "https://github.com/benjitaylor/agentation"
spec.license = "PolyForm-Shield-1.0.0"

spec.required_ruby_version = ">= 3.0.0"

spec.files = Dir[
"lib/**/*",
"app/**/*",
"LICENSE",
"README.md"
]

spec.require_paths = ["lib"]

spec.add_dependency "railties", ">= 6.0"
end
2,414 changes: 2,414 additions & 0 deletions rails/gem/app/assets/javascripts/agentation.js

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions rails/gem/lib/agentation-rails.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
require "agentation/engine"
require "agentation/configuration"
require "agentation/middleware"
35 changes: 35 additions & 0 deletions rails/gem/lib/agentation/configuration.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
module Agentation
class Configuration
attr_accessor :endpoint, :session_id, :webhook_url, :enabled, :copy_to_clipboard

def initialize
@endpoint = "http://localhost:4747"
@session_id = nil
@webhook_url = nil
@enabled = nil # auto-detect based on Rails.env
@copy_to_clipboard = nil # nil = use JS default (true)
end

def enabled?
if @enabled.nil?
defined?(Rails) && Rails.env.development?
else
@enabled
end
end
end

class << self
def configuration
@configuration ||= Configuration.new
end

def configure
yield(configuration)
end

def reset_configuration!
@configuration = Configuration.new
end
end
end
9 changes: 9 additions & 0 deletions rails/gem/lib/agentation/engine.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module Agentation
class Engine < ::Rails::Engine
initializer "agentation.middleware" do |app|
# Insert at the bottom of the stack (closest to the app) so we see the
# raw response before other body-modifying middleware (Bullet, ETag, etc.)
app.middleware.use Agentation::Middleware
end
end
end
93 changes: 93 additions & 0 deletions rails/gem/lib/agentation/middleware.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
require "erb"

module Agentation
ASSET_PATH = "/__agentation__/agentation.js"

class Middleware
def initialize(app)
@app = app
end

def call(env)
# Serve the JS file directly when requested
return serve_js(env) if env["PATH_INFO"] == ASSET_PATH

status, headers, response = @app.call(env)

return [status, headers, response] unless inject?(status, headers)

# Skip streaming responses — we can't buffer those
return [status, headers, response] if streaming?(headers, response)

body = +""
response.each { |part| body << part }
response.close if response.respond_to?(:close)

body = body.sub(%r{</head>}i, "#{head_tag}\n</head>")

config_tag = body_tag
body = body.sub(%r{</body>}i, "#{config_tag}\n</body>") if config_tag

headers.delete("Content-Length")

[status, headers, [body]]
end

private

def serve_js(_env)
[
200,
{
"Content-Type" => "application/javascript",
"Content-Length" => agentation_js.bytesize.to_s,
"Cache-Control" => "no-store"
},
[agentation_js]
]
end

def inject?(status, headers)
return false unless Agentation.configuration.enabled?
return false unless status == 200

content_type = headers["Content-Type"]
content_type&.include?("text/html")
end

def streaming?(_headers, response)
response.respond_to?(:stream) || !response.respond_to?(:each)
end

def head_tag
@head_tag ||= %(<script src="#{ASSET_PATH}" defer></script>)
end

# Recomputed each request so config changes via console/reloader take effect
def body_tag
config = Agentation.configuration
attrs = []
attrs << data_attr("endpoint", config.endpoint)
attrs << data_attr("session-id", config.session_id)
attrs << data_attr("webhook-url", config.webhook_url)
attrs << data_attr("copy-to-clipboard", config.copy_to_clipboard) unless config.copy_to_clipboard.nil?
attrs.compact!

return nil if attrs.empty?

%(<div id="agentation-config" style="display:none"#{attrs.join}></div>)
end

def data_attr(name, value)
return nil unless value

%( data-#{name}="#{ERB::Util.html_escape(value)}")
end

def agentation_js
@agentation_js ||= File.read(
File.expand_path("../../app/assets/javascripts/agentation.js", __dir__)
)
end
end
end
12 changes: 12 additions & 0 deletions rails/gem/lib/generators/agentation/install_generator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module Agentation
module Generators
class InstallGenerator < Rails::Generators::Base
desc "Creates an Agentation initializer in config/initializers."
source_root File.expand_path("templates", __dir__)

def copy_initializer
template "agentation.rb", "config/initializers/agentation.rb"
end
end
end
end
18 changes: 18 additions & 0 deletions rails/gem/lib/generators/agentation/templates/agentation.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Agentation — visual annotation toolbar for AI coding agents.
# https://github.com/benjitaylor/agentation
#
# The toolbar appears automatically in development with no configuration.
# Uncomment lines below to customize.
Agentation.configure do |config|
# MCP sync server endpoint (default: "http://localhost:4747")
# config.endpoint = "http://localhost:4747"

# Webhook URL for annotation events
# config.webhook_url = "https://example.com/hooks/agentation"

# Disable copy-to-clipboard (default: enabled)
# config.copy_to_clipboard = false

# Force enable/disable (default: auto-detects Rails.env.development?)
# config.enabled = true
end
Loading