agentsclimarketplace

Openui forge ruby

Skill OthmanAdi/openui-forge/.codex/skills/openui-forge-ruby

Cross-IDE, multi-stack agent skill for OpenUI (the Open Standard for Generative UI). Adds OpenUI to existing projects across 12 backend stacks, any LLM provider, and 11 agent platforms. Scaffold, integrate, validate.

Install
npx -y skills add OthmanAdi/openui-forge --skill openui-forge-ruby

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 20 stars20 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

OpenUI generative UI with a Ruby on Rails backend. SSE streaming via ActionController::Live, forwarding the OpenAI API stream with Net::HTTP.

SKILL.md

9.1 KB, as published. Nobody here has run it

OpenUI Forge — Ruby

Build generative UI apps with a React frontend + Ruby on Rails backend. Streams OpenAI API responses directly via ActionController::Live, forwarding OpenAI's native SSE with Net::HTTP.

Activation Triggers

  • "openui ruby", "openui rails", "openui ruby backend"
  • "generative ui ruby", "rails streaming ui backend"

Prerequisites

  • Node.js >= 22 (24 LTS recommended) + React >= 18.3.1 (19+ recommended) (frontend)
  • Ruby >= 3.2 + Rails 8.1.x (backend; run on Puma — ActionController::Live needs a threaded server, not WEBrick)
  • OPENAI_API_KEY environment variable set

Quick Start

  1. Create the React frontend and install OpenUI deps:
npm install @openuidev/react-ui @openuidev/react-headless @openuidev/react-lang lucide-react zod
  1. Generate the system prompt into the Rails app:
npx @openuidev/cli generate ./src/lib/library.ts --out config/system-prompt.txt
  1. Create the Rails backend (see Full Code below)
  2. Run: bin/rails server -p 3001 on :3001, frontend on :3000

Full Code

Backend: Gemfile

source "https://rubygems.org"

gem "rails", "~> 8.1"
gem "puma", ">= 6.0"
# Net::HTTP is in the standard library — no extra HTTP-client gem required.
# Optional: load OPENAI_API_KEY etc. from a .env file in development.
gem "dotenv-rails", groups: [:development, :test]

Backend: app/controllers/chat_controller.rb

require "net/http"
require "json"
require "uri"

class ChatController < ApplicationController
  include ActionController::Live

  skip_forgery_protection
  before_action :set_cors_headers
  before_action :handle_preflight, only: :create

  OPENAI_BASE_URL = ENV.fetch("OPENAI_BASE_URL", "https://api.openai.com/v1").freeze
  OPENAI_MODEL    = ENV.fetch("OPENAI_MODEL", "gpt-5.5").freeze

  # Loaded once at boot.
  SYSTEM_PROMPT = Rails.root.join("config", "system-prompt.txt").read.freeze

  # POST /api/chat  { "messages": [{ "role": "...", "content": "..." }] }
  def create
    api_key = ENV["OPENAI_API_KEY"]
    if api_key.nil? || api_key.empty?
      render(json: { error: "OPENAI_API_KEY not set" }, status: :internal_server_error)
      return
    end

    body = JSON.parse(request.body.read) rescue {}
    incoming = body["messages"]
    unless incoming.is_a?(Array) && !incoming.empty?
      render(json: { error: "messages must be a non-empty array" }, status: :bad_request)
      return
    end

    # Prepend the server-side system prompt; never trust a client-sent one.
    messages = [{ "role" => "system", "content" => SYSTEM_PROMPT }]
    incoming.each do |m|
      next unless m.is_a?(Hash)
      messages << { "role" => m["role"].to_s, "content" => m["content"].to_s }
    end

    # Headers MUST be set before the first write (the response commits on write).
    response.headers["Content-Type"]  = "text/event-stream"
    response.headers["Cache-Control"] = "no-cache"
    # Rails inserts Rack::ETag, which buffers the whole body and breaks
    # streaming. Setting Last-Modified makes Rack::ETag pass the body through.
    response.headers["Last-Modified"] = Time.now.httpdate
    # Defeat proxy buffering (nginx) so chunks reach the browser immediately.
    response.headers["X-Accel-Buffering"] = "no"

    uri = URI.parse("#{OPENAI_BASE_URL}/chat/completions")
    payload = JSON.generate(
      model: OPENAI_MODEL,
      stream: true,
      messages: messages,
    )

    # read_timeout: nil disables the default 60s per-read timeout; a streaming
    # completion can pause longer than 60s between chunks and would otherwise
    # raise Net::ReadTimeout and cut the response off mid-stream.
    Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", read_timeout: nil) do |http|
      upstream = Net::HTTP::Post.new(uri)
      upstream["Content-Type"]  = "application/json"
      upstream["Authorization"] = "Bearer #{api_key}"
      upstream["Accept"]        = "text/event-stream"
      upstream.body = payload

      http.request(upstream) do |res|
        unless res.code.to_i == 200
          err = +""
          res.read_body { |c| err << c }
          response.stream.write("data: #{JSON.generate(error: "OpenAI returned #{res.code}: #{err}")}\n\n")
          response.stream.write("data: [DONE]\n\n")
          next
        end

        # Forward OpenAI's native SSE bytes verbatim. The upstream already emits
        # `data: {chunk}\n\n` frames terminated by `data: [DONE]`, so we just
        # write each chunk through and flush — no buffering, no reframing. This
        # avoids splitting a frame that straddles a TCP read boundary, because
        # the browser's adapter reassembles SSE frames itself.
        res.read_body do |chunk|
          response.stream.write(chunk)
        end
      end
    end
  rescue IOError, Errno::EPIPE
    # Client disconnected mid-stream — nothing more to send.
  rescue => e
    Rails.logger.error("[chat] stream error: #{e.class}: #{e.message}")
    begin
      response.stream.write("data: #{JSON.generate(error: e.message)}\n\n")
      response.stream.write("data: [DONE]\n\n")
    rescue IOError, Errno::EPIPE
      # client already gone
    end
  ensure
    # ALWAYS close, or the socket leaks for the lifetime of the worker.
    response.stream.close
  end

  private

  # Lock CORS to the single configured frontend origin (NOT "*"): the request
  # is credentialed-capable and a wildcard would let any site spend your key.
  def set_cors_headers
    response.headers["Access-Control-Allow-Origin"]  = ENV.fetch("FRONTEND_ORIGIN", "http://localhost:3000")
    response.headers["Vary"]                         = "Origin"
    response.headers["Access-Control-Allow-Methods"] = "POST, OPTIONS"
    response.headers["Access-Control-Allow-Headers"] = "Content-Type, Authorization"
  end

  def handle_preflight
    head(:no_content) if request.method == "OPTIONS"
  end
end

Backend: config/routes.rb

Rails.application.routes.draw do
  post  "/api/chat", to: "chat#create"
  match "/api/chat", to: "chat#create", via: :options  # CORS preflight
end

Frontend: app/chat/page.tsx

"use client";
import { FullScreen } from "@openuidev/react-ui";
import { openuiChatLibrary } from "@openuidev/react-ui/genui-lib";
import {
  openAIAdapter,
  openAIMessageFormat,
} from "@openuidev/react-headless";

export default function ChatPage() {
  return (
    <FullScreen
      componentLibrary={openuiChatLibrary}
      streamProtocol={openAIAdapter()}
      messageFormat={openAIMessageFormat}
      apiUrl="http://localhost:3001/api/chat"
    />
  );
}

The Rails backend forwards OpenAI's SSE stream verbatim through ActionController::Live (response.stream.write(chunk) per Net::HTTP read_body chunk), so the client sees tokens as they arrive. Pair it with openAIAdapter() on the frontend. openAIReadableStreamAdapter() is for NDJSON (no data: prefix) and will silently produce no output here.

Net::HTTP (standard library) is the dependency-free default here. The ruby-openai / openai gems are alternatives if you want a typed client, but forwarding the raw SSE bytes needs no gem.

System Prompt Generation

npx @openuidev/cli generate ./src/lib/library.ts --out config/system-prompt.txt

Validation Checklist

  • config/system-prompt.txt exists in the Rails app
  • OPENAI_API_KEY is set in environment or .env
  • Controller include ActionController::Live and skip_forgery_protection
  • CORS headers allow the frontend origin (not *)
  • Response streams SSE directly from OpenAI API (verbatim passthrough)
  • response.stream.close runs in an ensure block
  • Last-Modified (or removed Rack::ETag) so the response is not buffered
  • Running on Puma, not WEBrick
  • Frontend apiUrl points to http://localhost:3001/api/chat
  • Frontend uses streamProtocol={openAIAdapter()} and openAIMessageFormat
  • componentLibrary={openuiChatLibrary} prop passed to FullScreen
  • CSS import in root layout (@openuidev/react-ui/components.css)

Error Patterns

ErrorCauseFix
CORS blockedOrigin mismatchSet FRONTEND_ORIGIN to match the frontend
system-prompt.txt missing (Errno::ENOENT at boot)File not generatedRun the CLI generate command into config/
Response arrives all at once, not streamedRack::ETag bufferingKeep the Last-Modified header (or remove Rack::ETag)
Stream hangs / never flushesRunning on WEBrickRun on Puma (threaded server)
ActionController::Live::ClientDisconnected / Errno::EPIPEClient closed the tab mid-streamRescue it; the ensure still closes the stream
Socket stays open after requestresponse.stream.close skippedClose in an ensure block (as shown)
422 Unprocessable Entity on POSTCSRF checkskip_forgery_protection in the controller
Empty responseBody not forwardedVerify the `read_body do

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.