Skip to content

discourse/discourse-command-center

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Discourse Command Center

A type-away admin command palette for Discourse. Admins open the existing admin search (Cmd/Ctrl + /) — or the inline bar on the admin dashboard — and type plain-language commands like suspend bob 3 weeks, create category 'radios', or grant admin to alice. The palette matches the command, shows a plan (an editable, pre-filled form), and only runs it once the admin confirms. Everything is re-resolved and re-authorized on the server.

At the moment it is deterministic (no LLM): commands declare trigger words/aliases and typed params, and a parser fills those params from what you type.

image

How it works

type "ban bob 3 weeks"
  │
  ├─ client matches the trigger "ban" against the cached catalog → shows the
  │  "Suspend a user" command in the palette (instant, no server call)
  │
  ├─ on select → POST /plan : the server tokenizes the text, resolves params
  │  (bob → User, "3 weeks" → a date), runs the guardian + checks, and
  │  returns a plan: each param as an editable field (pre-filled), a preview,
  │  whether it's allowed, and any "hits"
  │
  ├─ plan card: edit any field (user chooser, calendar, reason…), see warnings
  │
  └─ Confirm → POST /execute : the server RE-resolves the submitted values,
     RE-checks the guardian + blocking checks, runs the command, audits it,
     and returns the result (or a directive the client performs)

Core concepts:

  • Command — one class per action (lib/discourse_command_center/commands/). Declared once; from that the framework derives the matcher, the plan form, the audited execution, and (later) an LLM tool schema.
  • service macro — wraps an existing Service::Base (e.g. User::Suspend) and imports its contract (param names, types, required-ness) automatically.
  • Param typesuser, category, group, tag, duration, datetime, string, etc. Each knows how to parse text, resolve to a record, render a control, and re-coerce on execute.
  • guardian — hard permission gate (disables Confirm). check — pre-flight "hit": a non-blocking heads-up, or blocking: true (e.g. "already suspended") which disables Confirm and is enforced server-side.
  • directive — for actions that can't run in one request (impersonate sets the session; grant admin needs 2FA), execute returns a directive the client performs instead of mutating.

Where it shows up

  • Cmd/Ctrl + / modal — the full palette (commands + user quick actions + core admin search results).
  • Admin dashboard — an inline bar at the top (admin-dashboard-top outlet) that floats results as a popover.
  • Admin → Plugins → Command center → Commands — a reference page listing every command with its triggers and params.

Relevant files

Backend (lib/ + app/)

File What it is
lib/discourse_command_center/command.rb Base class + DSL (identifier, triggers, param, service, guardian, check, plan, execute, result_link)
lib/discourse_command_center/param_types.rb Param types — parsing/resolution/coercion (incl. Duration natural-language dates)
lib/discourse_command_center/parser.rb Deterministic text → command + slot-filled params
lib/discourse_command_center/registry.rb Collects commands (self-registered + other plugins)
lib/discourse_command_center/result.rb Execution result (success/error/directive)
lib/discourse_command_center/commands/*.rb The commands (suspend, silence, create category/group, grant admin, impersonate)
app/controllers/discourse_command_center/commands_controller.rb /catalog, /plan, /execute (admin-only)

Frontend

File What it is
assets/javascripts/discourse/connectors/admin-search-palette/command-center.gjs Renders the palette inside admin search (via the core admin-search-palette outlet)
assets/javascripts/discourse/components/admin-command-plan-card.gjs Plan-mode card (editable pills/fields + Confirm)
assets/javascripts/discourse/lib/command-catalog.js Cached catalog + client-side trigger matching
admin/assets/javascripts/discourse/connectors/admin-dashboard-top/command-center.gjs Inline dashboard bar
admin/assets/javascripts/discourse/.../command-list.{js,gjs} The admin reference page (route + template)
assets/stylesheets/command-center.scss All styles

Config / meta

  • plugin.rb, config/routes.rb, config/settings.yml, config/locales/*
  • TESTING.md — manual test script + a seed runner (script/seed_test_scenarios.rb)
  • .claude/skills/discourse-command-center-authoring/ — skill for adding commands

Note: this plugin relies on a few small core outlets (admin-search-palette, admin-dashboard-top) and a @closeModal passthrough in core admin search.

Adding a command

Add a file under lib/discourse_command_center/commands/ — it auto-registers and auto-appears in the palette, the browse list, and the reference page. The fastest path is the service macro. See .claude/skills/discourse-command-center-authoring/SKILL.md for the full guide, and commands/suspend_user.rb (service-backed) / commands/create_category.rb (inline) as templates.

Adding commands from another plugin

Other plugins can contribute commands without modifying this plugin. Subclass DiscourseCommandCenter::Command (you get the same DSL — service, param, guardian, check, plan, execute, …) and register the class with DiscoursePluginRegistry.register_command_center_command(MyCommand, self). The register is gated by your plugin's enabled state: the command appears in the palette/catalog/reference page while your plugin is enabled, and disappears when it's disabled.

# plugins/discourse-foo/lib/discourse_foo/commands/message_user.rb
module ::DiscourseFoo
  module Commands
    class MessageUser < ::DiscourseCommandCenter::Command
      identifier :message_user
      title       "discourse_foo.command_center.message_user.title"  # your i18n key
      icon        "envelope"
      triggers    "message", "pm"

      param :user,    :user,   required: true
      param :message, :string, required: true, labels: %w[message saying]

      guardian { |g, _r| g.is_staff? }
      plan { |r| "Message @#{r[:user]&.username}" }

      execute do |resolved:, guardian:|
        # ...send the PM...
        DiscourseCommandCenter::Result.success(message: "Message sent.")
      end
    end
  end
end
# plugins/discourse-foo/plugin.rb
after_initialize do
  # Only wire it up if the command center is installed.
  if defined?(::DiscourseCommandCenter)
    require_relative "lib/discourse_foo/commands/message_user"
    DiscoursePluginRegistry.register_command_center_command(
      ::DiscourseFoo::Commands::MessageUser,
      self, # your Plugin::Instance — gates the command on this plugin
    )
  end
end

Register with your plugin instance (self in plugin.rb) so the gating works — don't rely on subclassing alone. The command then behaves exactly like a built-in: it's matched client-side, planned, and executed (re-resolved and re-authorized) through the same endpoints.

Settings & endpoints

  • Site setting: command_center_enabled (default off).
  • JSON API (admin-only), mounted at /admin/command-center: GET /catalog.json, POST /plan.json, POST /execute.json.

Tests

bin/rspec plugins/discourse-command-center/spec/lib plugins/discourse-command-center/spec/requests
bin/qunit --standalone plugins/discourse-command-center/test/javascripts

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors