- name
- stimulus
- description
- Client-side interactivity — phlex-reactive client ops, actions and broadcasts first, then existing Stimulus and Turbo Stream code, and the operator-confirmed exception. Use when adding any interaction (toggle, dropdown, live update, inline edit) or changing an existing Stimulus controller.
- argument-hint
- interaction or controller task
- allowed-tools
- Bash(*), Read, Write, Edit, Glob, Grep, Skill, AskUserQuestion
- model
- sonnet
# Interactivity: $ARGUMENTS
**Read the repository first.** `AGENTS.md` → `## Workflow` → **Interactivity** says which order this
app follows and whether new Stimulus or Turbo Stream code needs a human's confirmation. When it names
phlex-reactive, the order below is binding. When it does not (Stimulus only, or legacy JavaScript),
skip to "Maintaining Stimulus": new behaviour is a Stimulus controller.
## phlex-reactive first, in this order
1. **Client-only** (open/close, show/hide, pick one to reveal a panel, focus, copy):
`Phlex::Reactive::ClientBindings` with `on_client` ops (`js.show`/`hide`/`toggle`, `global: true`
beyond the root, `outside: true` for outside-close), `reactive_show`, `reactive_filter`,
`reactive_compute`. No round trip, no feature JavaScript (a custom `reactive_compute` reducer is
the exception you register).
```ruby
div(**reactive_root) do
Button(**on_client(:click, js.toggle("#details"))) { t("things.show.details") }
div(id: "details", hidden: true) { … }
end
```
2. **The server decides or persists:** a reactive `action` on a `Phlex::Reactive::Component`,
answering with `reply.morph`/`reply.replace`/`reply.also`. Authorize inside the action (Workflow →
Authorization); `skip_verify_authorized` only with a comment saying why it is public.
3. **Live updates to other viewers:** `broadcast_to`, not `Turbo::Broadcastable`.
4. **phlex-reactive cannot express it, or expresses it badly:** `/flow:plan upstream
zoolutions/phlex-reactive <the gap>` (a PR when the fix is small). Link it from our PR and from a
comment at the workaround.
5. **Only then** a new Stimulus controller or new Turbo Stream code, and only after a **human
operator confirms**: ask with AskUserQuestion, naming the upstream issue and why reactive cannot do
it, and record the answer in the PR body under `## Interactivity decision`. An agent never makes
this call; an unattended run parks the issue instead.
Not gated: Hotwire Native `bridge--*` controllers, Turbo Frames, Turbo Drive navigation.
**Count the JavaScript** in an app that loads the reactive client lazily per page: a reactive root
makes its page import the client, so a root in a layout, header or global dialog puts it on every
page. Measure the page weight and say so in the PR.
## Maintaining Stimulus and Turbo Streams
- Existing controllers stay until a change already touches them. Fix them in place, don't grow them;
migrate one to phlex-reactive when your change is in its path.
- Declare every `static targets`/`values`; wire with `data-action`, never inline `onclick` (CSP).
- Release in `disconnect()` what `connect()` acquired (listeners on `window`/`document`, timers,
observers). `connect()` runs again after every Turbo visit and morph: keep it idempotent.
- No module-level state; no new jQuery outside the legacy code that already uses it.
- After adding a controller, confirm it is registered (import map pin or controllers index) and loads
on the page that uses it.
- Existing Turbo Stream responses keep a stable target `id` on both sides.
## Test and debug
- Every interaction gets a system spec that ends on what the server stored, not on the click. After
each reactive interaction, call the wait helper Workflow → Specs names.
- A reactive click that does nothing, a 400/403/404 from the actions endpoint, or
`AuthorizationNotVerified`: the app's `phlex-reactive-debugging` skill (generated by the gem,
`rails g phlex:reactive:claude`).
Done when the system spec is green and Workflow → Verify passes.
View on GitHub