layered-assistant-rails 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/layered-assistant-rails/SKILL.md +207 -0
  3. data/AGENTS.md +4 -5
  4. data/README.md +19 -3
  5. data/app/assets/images/layered_assistant/icon_assistants.svg +3 -0
  6. data/app/assets/images/layered_assistant/icon_conversations.svg +3 -0
  7. data/app/assets/images/layered_assistant/icon_personas.svg +3 -0
  8. data/app/assets/images/layered_assistant/icon_providers.svg +3 -0
  9. data/app/assets/images/layered_assistant/icon_skills.svg +3 -0
  10. data/app/assets/images/layered_assistant/icon_spanner.svg +3 -0
  11. data/app/views/layered/assistant/assistants/edit.html.erb +4 -4
  12. data/app/views/layered/assistant/assistants/index.html.erb +2 -3
  13. data/app/views/layered/assistant/assistants/new.html.erb +4 -4
  14. data/app/views/layered/assistant/conversations/edit.html.erb +4 -4
  15. data/app/views/layered/assistant/conversations/index.html.erb +6 -9
  16. data/app/views/layered/assistant/conversations/new.html.erb +4 -4
  17. data/app/views/layered/assistant/messages/index.html.erb +4 -4
  18. data/app/views/layered/assistant/models/edit.html.erb +7 -4
  19. data/app/views/layered/assistant/models/index.html.erb +6 -7
  20. data/app/views/layered/assistant/models/new.html.erb +7 -4
  21. data/app/views/layered/assistant/personas/edit.html.erb +5 -1
  22. data/app/views/layered/assistant/personas/index.html.erb +2 -3
  23. data/app/views/layered/assistant/personas/new.html.erb +5 -1
  24. data/app/views/layered/assistant/providers/edit.html.erb +4 -4
  25. data/app/views/layered/assistant/providers/index.html.erb +2 -3
  26. data/app/views/layered/assistant/providers/new.html.erb +4 -4
  27. data/app/views/layered/assistant/public/assistants/index.html.erb +1 -1
  28. data/app/views/layered/assistant/setup/index.html.erb +2 -1
  29. data/app/views/layered/assistant/skills/edit.html.erb +5 -1
  30. data/app/views/layered/assistant/skills/index.html.erb +2 -3
  31. data/app/views/layered/assistant/skills/new.html.erb +5 -1
  32. data/app/views/layouts/layered/assistant/application.html.erb +11 -7
  33. data/lib/generators/layered/assistant/install_agent_skill_generator.rb +26 -0
  34. data/lib/generators/layered/assistant/install_generator.rb +0 -32
  35. data/lib/layered/assistant/engine.rb +1 -0
  36. data/lib/layered/assistant/version.rb +1 -1
  37. metadata +18 -9
  38. data/app/assets/tailwind/layered/assistant/styles.css +0 -13
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 609342ee9956936967616a13c2d86a350cbbf099875cc5bc5321730dd0280c71
4
- data.tar.gz: 89adcbc8c2f24834724e710427f7c7acc93d2d444b78d11d2005e3eb80d171cb
3
+ metadata.gz: 9419d936b5247ec519e481b02b7ead93a0e85bd6720a7b4298d145810c1405be
4
+ data.tar.gz: 5f9058cdae99fa50b3bf704853b74205f9a544e8c8430611ceb6ba1a509356bf
5
5
  SHA512:
6
- metadata.gz: a8e997c64ebdb5685d15571096f7539887995392850f68e1454cd579de82294d7c0eb6bbcae433984d8afabe62674b568fb13b241605b46827bbbd0e4476b871
7
- data.tar.gz: 00dd1808b570ff2204ba9866f589b5bc0ab159dd223846d3a9c6f12cbad6e685fb4d6a46e2c074b8c1f55a503ac83ac8b0c68a16f93c63a26a3fc665f9620bc9
6
+ metadata.gz: d5111d8c91517082ac5ab5eea935df4fbf73f0952c7455fcf6ac98f1fadab19f842aa0bf2483c60cdf1d8aaf2cfdb2cb812f488e7352195586038337f3e2314a
7
+ data.tar.gz: 3776256743b2734f9ae369508770e8cdc327e17299de34d01af376ba594485b4e55d2b160ce4b4820968c657fedcca2ede227067810a7dcb6f134e29dc5dd7ca
@@ -0,0 +1,207 @@
1
+ ---
2
+ name: layered-assistant-rails
3
+ description: Installs, configures, and builds with the layered-assistant-rails gem - a Rails 8+ engine providing AI assistant UI, conversations, providers, models, personas and skills, with a side-panel chat. Use when adding layered-assistant-rails to a Rails app, mounting the engine, configuring authorization and scoping, embedding the assistant panel, or troubleshooting setup.
4
+ license: Apache-2.0
5
+ compatibility: Requires Ruby on Rails >= 8.0, Ruby >= 3.2, layered-ui-rails >= 0.4, importmap-rails >= 2.0, stimulus-rails >= 1.0, turbo-rails
6
+ metadata:
7
+ author: layered.ai
8
+ version: "1.0"
9
+ source: https://github.com/layered-ai-public/layered-assistant-rails
10
+ ---
11
+
12
+ # layered-assistant-rails
13
+
14
+ A Rails 8+ engine providing an AI assistant: conversations, messages with streaming, configurable providers (Anthropic, OpenAI), models, personas, skills, and a side-panel chat that drops into any layered-ui-rails layout.
15
+
16
+ It builds on `layered-ui-rails` for layout, components, and theming. Install that gem first (or let this generator install it for you).
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ bundle add layered-assistant-rails
22
+ bin/rails generate layered:assistant:install
23
+ bin/rails db:migrate
24
+ ```
25
+
26
+ The install generator:
27
+
28
+ 1. Verifies `layered-ui-rails` is installed - invokes its installer if not.
29
+ 2. Adds `import "layered_assistant"` to `app/javascript/application.js` (after the `layered_ui` import).
30
+ 3. Creates `config/initializers/layered_assistant.rb` with example `authorize` and `scope` blocks.
31
+ 4. Mounts the engine at `/layered/assistant` in `config/routes.rb`.
32
+ 5. Copies migrations into the host app via the `layered:assistant:migrations` generator.
33
+
34
+ After installation, configure the initializer (see below) - **routes return 403 until an `authorize` block is set**. Then visit `/layered/assistant` to set up providers, models, personas, skills and assistants.
35
+
36
+ ## Authorization
37
+
38
+ All non-public engine routes are gated by an `authorize` block. The block runs in controller context, so you have access to `current_user`, `request`, `redirect_to`, `head`, `main_app`, etc.
39
+
40
+ ```ruby
41
+ # config/initializers/layered_assistant.rb
42
+
43
+ # Require sign-in (Devise):
44
+ Layered::Assistant.authorize do
45
+ redirect_to main_app.new_user_session_path unless user_signed_in?
46
+ end
47
+
48
+ # Restrict to admins:
49
+ Layered::Assistant.authorize do
50
+ head :forbidden unless current_user&.admin?
51
+ end
52
+
53
+ # Allow all (no-op):
54
+ Layered::Assistant.authorize do
55
+ end
56
+ ```
57
+
58
+ Until configured, every request returns 403. Public routes under `/layered/assistant/public/...` are exempt - use these to expose specific assistants to anonymous visitors.
59
+
60
+ ## Scoping (multi-tenant ownership)
61
+
62
+ Engine models with a polymorphic `owner` association (assistants, personas, providers, models, skills, conversations) can be scoped per request. The block receives the model class and returns an `ActiveRecord::Relation`:
63
+
64
+ ```ruby
65
+ Layered::Assistant.scope do |model_class|
66
+ model_class.where(owner: current_user)
67
+ end
68
+ ```
69
+
70
+ Scope only conversations, leave the rest unscoped:
71
+
72
+ ```ruby
73
+ Layered::Assistant.scope do |model_class|
74
+ if model_class == Layered::Assistant::Conversation
75
+ model_class.where(owner: current_user)
76
+ else
77
+ model_class.all
78
+ end
79
+ end
80
+ ```
81
+
82
+ Ownership is enforced **at the controller layer via `scoped()`**, not via model validations. Out-of-scope IDs return 404.
83
+
84
+ ## Optional settings
85
+
86
+ ```ruby
87
+ Layered::Assistant.log_errors = true # log API errors to stdout
88
+ Layered::Assistant.api_request_timeout = 210 # total streaming API timeout (seconds)
89
+ Layered::Assistant.skip_db_encryption = true # dev/test only - skip encryption on Provider#secret
90
+ ```
91
+
92
+ `Provider#secret` is encrypted with Rails encrypted attributes, so the host app must have `bin/rails db:encryption:init` keys configured (or set `skip_db_encryption = true` for dev/test).
93
+
94
+ ## Mounting the assistant panel
95
+
96
+ The engine ships a side-panel chat that plugs into the `layered-ui-rails` panel slot. Use `PanelHelper` inside your layout's `content_for` blocks (always **above** the layout render call):
97
+
98
+ ```erb
99
+ <% content_for :l_ui_panel_heading do %>
100
+ <%= layered_assistant_panel_header %>
101
+ <% end %>
102
+
103
+ <% content_for :l_ui_panel_body do %>
104
+ <%= layered_assistant_panel_body %>
105
+ <% end %>
106
+
107
+ <%= render template: "layouts/layered_ui/application" %>
108
+ ```
109
+
110
+ The body lazy-loads the conversation list from `panel_conversations_path` via a Turbo Frame.
111
+
112
+ For a public (unauthenticated) panel scoped to one assistant:
113
+
114
+ ```erb
115
+ <% content_for :l_ui_panel_body do %>
116
+ <%= layered_assistant_public_panel_body(assistant: @assistant) %>
117
+ <% end %>
118
+ ```
119
+
120
+ ## View helpers
121
+
122
+ | Helper | Purpose |
123
+ |---|---|
124
+ | `layered_assistant_panel_header(**opts, &block)` | Turbo frame for the panel header |
125
+ | `layered_assistant_panel_body(**opts)` | Turbo frame that loads the authenticated panel conversations |
126
+ | `layered_assistant_public_panel_body(assistant:, **opts)` | Turbo frame for an unauthenticated single-assistant panel |
127
+ | `l_assistant_accessible?` | Returns true if the current request would pass the `authorize` block - useful for hiding the panel from unauthorised users |
128
+
129
+ Example - only render the panel for users who can access it:
130
+
131
+ ```erb
132
+ <% if l_assistant_accessible? %>
133
+ <% content_for :l_ui_panel_heading do %>
134
+ <%= layered_assistant_panel_header %>
135
+ <% end %>
136
+ <% content_for :l_ui_panel_body do %>
137
+ <%= layered_assistant_panel_body %>
138
+ <% end %>
139
+ <% end %>
140
+ ```
141
+
142
+ ## Models
143
+
144
+ All under `Layered::Assistant::*`, tables prefixed `layered_assistant_`. Inherit from `Layered::Assistant::ApplicationRecord`.
145
+
146
+ | Model | Purpose |
147
+ |---|---|
148
+ | `Provider` | API provider config (Anthropic, OpenAI). Holds the encrypted `secret` (API key) |
149
+ | `Model` | A specific model offered by a `Provider` (e.g. `claude-opus-4-7`) |
150
+ | `Persona` | Reusable system prompt / character |
151
+ | `Skill` | A capability that can be attached to assistants |
152
+ | `Assistant` | A configured assistant: model + persona + skills |
153
+ | `AssistantSkill` | Join between assistant and skill |
154
+ | `Conversation` | A chat session with an assistant, owned polymorphically |
155
+ | `Message` | A single message in a conversation; supports streaming |
156
+
157
+ Enums are stored as **strings**, not integers.
158
+
159
+ ## Routes
160
+
161
+ Mounted at `/layered/assistant` by default. Top-level resources: `personas`, `skills`, `assistants` (with nested `conversations`), `providers` (with nested `models`), `conversations` (with nested `messages` and a `stop` member route).
162
+
163
+ The engine also exposes:
164
+
165
+ - `panel/conversations` and `panel/conversations/:id/messages` - the authenticated side-panel API
166
+ - `public/assistants`, `public/conversations`, `public/panel/conversations` - unauthenticated entry points for embedding a single assistant on a public page
167
+
168
+ Use `layered_assistant.<route>_path` from the host app, or `main_app.<route>_path` from inside engine views.
169
+
170
+ ## JavaScript
171
+
172
+ Stimulus controllers are registered automatically via importmap once `import "layered_assistant"` is in `application.js`:
173
+
174
+ | Identifier | Purpose |
175
+ |---|---|
176
+ | `composer` | Message composer (textarea autosize, submit-on-enter) |
177
+ | `messages` | Message list rendering and scroll behaviour |
178
+ | `panel` | Assistant side-panel state |
179
+ | `panel-nav` | Conversation list navigation inside the panel |
180
+ | `conversation-select` | Conversation picker |
181
+ | `provider-template` | Provider form prefill from a template |
182
+
183
+ `message_streaming.js` wires SSE streaming for assistant replies.
184
+
185
+ ## Styling
186
+
187
+ The engine renders inside `layered-ui-rails` layouts and uses only `l-ui-` classes - no host-app Tailwind utilities are required, and **no engine-only Tailwind classes leak into views** (the host's Tailwind build does not scan engine view files). If you customise the look, theme via `layered-ui-rails` CSS custom properties.
188
+
189
+ ## Conventions
190
+
191
+ - **Locale**: en-GB unless a technical standard dictates otherwise.
192
+ - **Ownership**: enforce via `scoped()` in controllers, not model validations.
193
+ - **No bundler**: importmap only.
194
+ - **Accessibility**: WCAG 2.2 AA - tables include `<caption>`, `scope="col"`/`scope="row"`, etc.
195
+
196
+ ## Common issues
197
+
198
+ - **All routes return 403** - no `authorize` block is configured. Edit `config/initializers/layered_assistant.rb`.
199
+ - **Provider creation fails with encryption error** - run `bin/rails db:encryption:init` and add the keys to credentials, or set `Layered::Assistant.skip_db_encryption = true` for dev/test.
200
+ - **Panel body never loads** - `turbo-rails` must be installed and `layered-ui-rails` must be mounted in the layout. Check `import "@hotwired/turbo-rails"` is present.
201
+ - **`layered_assistant` JS controllers missing** - ensure `import "layered_assistant"` is in `app/javascript/application.js` (added by the install generator, after the `layered_ui` import).
202
+ - **Cross-tenant records visible** - configure `Layered::Assistant.scope` to filter by `owner`.
203
+
204
+ ## Further reference
205
+
206
+ - Repository: https://github.com/layered-ai-public/layered-assistant-rails
207
+ - Companion gem: `layered-ui-rails` - layout, components, helpers (skill `layered-ui-rails`)
data/AGENTS.md CHANGED
@@ -4,9 +4,9 @@ This file provides guidance to AI agents when working with code in this reposito
4
4
 
5
5
  ## Project Overview
6
6
 
7
- **layered-assistant-rails** is a Rails 8+ engine gem (`Layered::Assistant`) providing AI assistant UI components. It uses `isolate_namespace Layered::Assistant` and distributes assets (Tailwind CSS + importmap JS) to host applications via a generator.
7
+ **layered-assistant-rails** is a Rails 8+ engine gem (`Layered::Assistant`) providing AI assistant UI components. It uses `isolate_namespace Layered::Assistant` and distributes JS (importmap) to host applications via a generator.
8
8
 
9
- Requires Rails >= 8.0.0, Ruby >= 3.2.0. Depends on sibling gem `layered-ui-rails` (path dependency at `../layered-ui-rails`).
9
+ Requires Rails >= 8.0.0, Ruby >= 3.3.0. Depends on sibling gem `layered-ui-rails` (path dependency at `../layered-ui-rails`).
10
10
 
11
11
  ## Architecture
12
12
 
@@ -18,12 +18,11 @@ Requires Rails >= 8.0.0, Ruby >= 3.2.0. Depends on sibling gem `layered-ui-rails
18
18
 
19
19
  ### Asset Distribution
20
20
 
21
- CSS and JS are authored in the engine and delivered to host apps:
21
+ JS is authored in the engine and delivered to host apps:
22
22
 
23
- - **CSS**: `app/assets/tailwind/layered/assistant/styles.css` — copied to host app by the install generator
24
23
  - **JS**: `app/javascript/layered_assistant/index.js` — pinned via `config/importmap.rb`, made available through engine initializer
25
24
 
26
- The install generator (`lib/generators/layered/assistant/install_generator.rb`) verifies `layered-ui-rails` is installed first, then copies CSS and injects import lines into `application.css` and `application.js`.
25
+ The install generator (`lib/generators/layered/assistant/install_generator.rb`) verifies `layered-ui-rails` is installed first, then injects the JS import line into `application.js`.
27
26
 
28
27
  ### Test Harness
29
28
 
data/README.md CHANGED
@@ -15,6 +15,24 @@ An open source Rails 8+ engine built on [layered-ui-rails](https://github.com/la
15
15
  - Ruby on Rails >= 8.0
16
16
  - [layered-ui-rails](https://github.com/layered-ai-public/layered-ui-rails) installed in the host app
17
17
 
18
+ ## Agent skill
19
+
20
+ An [agent skill](https://agentskills.io) is included so AI coding agents can work with `layered-assistant-rails` in your project. Once installed, the agent can handle the full setup - just ask it to add `layered-assistant-rails` to your app and it will install the gem, run the generator, and configure your layout.
21
+
22
+ **Project install** - scoped to a single repo, available to all contributors:
23
+
24
+ ```bash
25
+ bin/rails generate layered:assistant:install_agent_skill
26
+ ```
27
+
28
+ **Global install** - available across all your projects:
29
+
30
+ ```bash
31
+ ./install-skill.sh
32
+ # or install remotely without cloning the repo:
33
+ curl -fsSL https://raw.githubusercontent.com/layered-ai-public/layered-assistant-rails/main/install-skill.sh | sh
34
+ ```
35
+
18
36
  ## Installation
19
37
 
20
38
  Add to your Gemfile:
@@ -33,7 +51,7 @@ bundle install
33
51
 
34
52
  ### Install generator
35
53
 
36
- Run the install generator to copy CSS and register imports:
54
+ Run the install generator to register imports and mount the engine:
37
55
 
38
56
  ```bash
39
57
  bin/rails generate layered:assistant:install
@@ -43,8 +61,6 @@ This will:
43
61
  - Copy `layered_ui.css` to `app/assets/tailwind/`
44
62
  - Add `@import "./layered_ui";` to your `application.css`
45
63
  - Add `import "layered_ui"` to your `application.js`
46
- - Copy `layered_assistant.css` to `app/assets/tailwind/layered_assistant.css`
47
- - Add `@import "./layered_assistant";` to your `app/assets/tailwind/application.css` (after the layered-ui import)
48
64
  - Add `import "layered_assistant"` to your `app/javascript/application.js` (after the layered-ui import)
49
65
  - Mount the engine at `/layered/assistant` in your `config/routes.rb`
50
66
  - Copy engine migrations into your application
@@ -0,0 +1,3 @@
1
+ <svg fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
2
+ <path stroke-linecap="round" stroke-linejoin="round" d="M19.5 12a7.5 7.5 0 0 0-15 0v5.25a1.5 1.5 0 0 0 1.5 1.5h1.5a1.5 1.5 0 0 0 1.5-1.5V13.5a1.5 1.5 0 0 0-1.5-1.5H4.5m15 0h-3a1.5 1.5 0 0 0-1.5 1.5v3.75a1.5 1.5 0 0 0 1.5 1.5h.75m2.25-5.25v6a3 3 0 0 1-3 3h-3" />
3
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
2
+ <path stroke-linecap="round" stroke-linejoin="round" d="M8.625 9.75a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0H8.25m4.125 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0H12m4.125 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0h-.375m-13.5 3.01c0 1.6 1.123 2.994 2.707 3.227 1.087.16 2.185.283 3.293.369V21l4.184-4.183a1.14 1.14 0 0 1 .778-.332 48.294 48.294 0 0 0 5.83-.498c1.585-.233 2.708-1.626 2.708-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0 0 12 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018Z" />
3
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
2
+ <path stroke-linecap="round" stroke-linejoin="round" d="M17.982 18.725A7.488 7.488 0 0 0 12 15.75a7.488 7.488 0 0 0-5.982 2.975m11.963 0a9 9 0 1 0-11.963 0m11.963 0A8.966 8.966 0 0 1 12 21a8.966 8.966 0 0 1-5.982-2.275M15 9.75a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
3
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
2
+ <path stroke-linecap="round" stroke-linejoin="round" d="M5.25 14.25h13.5m-13.5 0a3 3 0 0 1-3-3m3 3a3 3 0 1 0 0 6h13.5a3 3 0 1 0 0-6m-16.5-3a3 3 0 0 1 3-3h13.5a3 3 0 0 1 3 3m-19.5 0a4.5 4.5 0 0 1 .9-2.7L5.737 5.1a3.375 3.375 0 0 1 2.7-1.35h7.126c1.062 0 2.062.5 2.7 1.35l2.587 3.45a4.5 4.5 0 0 1 .9 2.7m0 0a3 3 0 0 1-3 3m0 3h.008v.008h-.008v-.008Zm0-6h.008v.008h-.008v-.008Zm-3 6h.008v.008h-.008v-.008Zm0-6h.008v.008h-.008v-.008Z" />
3
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
2
+ <path stroke-linecap="round" stroke-linejoin="round" d="M9.813 15.904 9 18.75l-.813-2.846a4.5 4.5 0 0 0-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 0 0 3.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 0 0 3.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 0 0-3.09 3.09ZM18.259 8.715 18 9.75l-.259-1.035a3.375 3.375 0 0 0-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 0 0 2.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 0 0 2.456 2.456L21.75 6l-1.035.259a3.375 3.375 0 0 0-2.456 2.456ZM16.894 20.567 16.5 21.75l-.394-1.183a2.25 2.25 0 0 0-1.423-1.423L13.5 18.75l1.183-.394a2.25 2.25 0 0 0 1.423-1.423l.394-1.183.394 1.183a2.25 2.25 0 0 0 1.423 1.423l1.183.394-1.183.394a2.25 2.25 0 0 0-1.423 1.423Z" />
3
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
2
+ <path stroke-linecap="round" stroke-linejoin="round" d="M11.42 15.17 17.25 21A2.652 2.652 0 0 0 21 17.25l-5.877-5.877M11.42 15.17l2.496-3.03c.317-.384.74-.626 1.208-.766M11.42 15.17l-4.655 5.653a2.548 2.548 0 1 1-3.586-3.586l6.837-5.63m5.108-.233c.55-.164 1.163-.188 1.743-.14a4.5 4.5 0 0 0 4.486-6.336l-3.276 3.277a3.004 3.004 0 0 1-2.25-2.25l3.276-3.276a4.5 4.5 0 0 0-6.336 4.486c.091 1.076-.071 2.264-.904 2.95l-.102.085m-1.745 1.437L5.909 7.5H4.5L2.25 3.75l1.5-1.5L7.5 4.5v1.409l4.26 4.26m-1.745 1.437 1.745-1.437m6.615 8.206L15.75 15.75M4.867 19.125h.008v.008h-.008v-.008Z" />
3
+ </svg>
@@ -1,6 +1,6 @@
1
- <div class="l-ui-container--spread">
2
- <h1>Edit assistant</h1>
3
- <%= link_to "Back", layered_assistant.assistants_path, class: "l-ui-button--outline" %>
4
- </div>
1
+ <%= l_ui_title_bar(
2
+ title: "Edit assistant",
3
+ breadcrumbs: [["Assistants", layered_assistant.assistants_path]]
4
+ ) %>
5
5
 
6
6
  <%= render "form", assistant: @assistant, url: layered_assistant.assistant_path(@assistant) %>
@@ -1,7 +1,6 @@
1
- <div class="l-ui-container--spread">
2
- <h1>Assistants</h1>
1
+ <%= l_ui_title_bar(title: "Assistants") do %>
3
2
  <%= link_to "New", layered_assistant.new_assistant_path, class: "l-ui-button--primary" %>
4
- </div>
3
+ <% end %>
5
4
 
6
5
  <div class="l-ui-container--table l-ui-utility--mt-lg">
7
6
  <table class="l-ui-table">
@@ -1,6 +1,6 @@
1
- <div class="l-ui-container--spread">
2
- <h1>New assistant</h1>
3
- <%= link_to "Back", layered_assistant.assistants_path, class: "l-ui-button--outline" %>
4
- </div>
1
+ <%= l_ui_title_bar(
2
+ title: "New assistant",
3
+ breadcrumbs: [["Assistants", layered_assistant.assistants_path]]
4
+ ) %>
5
5
 
6
6
  <%= render "form", assistant: @assistant, url: layered_assistant.assistants_path %>
@@ -1,6 +1,6 @@
1
- <div class="l-ui-container--spread">
2
- <h1>Edit Conversation</h1>
3
- <%= link_to "Back", layered_assistant.conversations_path, class: "l-ui-button--outline" %>
4
- </div>
1
+ <%= l_ui_title_bar(
2
+ title: "Edit conversation",
3
+ breadcrumbs: [["Conversations", layered_assistant.conversations_path]]
4
+ ) %>
5
5
 
6
6
  <%= render "form", conversation: @conversation, url: layered_assistant.conversation_path(@conversation) %>
@@ -1,12 +1,9 @@
1
- <div class="l-ui-container--spread">
2
- <h1><%= @assistant ? "Conversations - #{@assistant.name}" : "Conversations" %></h1>
3
- <div class="l-ui-container--spread">
4
- <% if @assistant %>
5
- <%= link_to "Back", layered_assistant.assistants_path, class: "l-ui-button--outline" %>
6
- <% end %>
7
- <%= link_to "New", layered_assistant.new_conversation_path(conversation: @assistant ? { assistant_id: @assistant.id } : {}), class: "l-ui-button--primary" %>
8
- </div>
9
- </div>
1
+ <%= l_ui_title_bar(
2
+ title: @assistant ? "Conversations - #{@assistant.name}" : "Conversations",
3
+ breadcrumbs: @assistant ? [["Assistants", layered_assistant.assistants_path]] : []
4
+ ) do %>
5
+ <%= link_to "New", layered_assistant.new_conversation_path(conversation: @assistant ? { assistant_id: @assistant.id } : {}), class: "l-ui-button--primary" %>
6
+ <% end %>
10
7
 
11
8
  <div class="l-ui-container--table l-ui-utility--mt-lg">
12
9
  <table class="l-ui-table">
@@ -1,6 +1,6 @@
1
- <div class="l-ui-container--spread">
2
- <h1>New Conversation</h1>
3
- <%= link_to "Back", layered_assistant.conversations_path, class: "l-ui-button--outline" %>
4
- </div>
1
+ <%= l_ui_title_bar(
2
+ title: "New conversation",
3
+ breadcrumbs: [["Conversations", layered_assistant.conversations_path]]
4
+ ) %>
5
5
 
6
6
  <%= render "form", conversation: @conversation, url: layered_assistant.conversations_path %>
@@ -1,7 +1,7 @@
1
- <div class="l-ui-container--spread">
2
- <h1>Messages</h1>
3
- <%= link_to "Back", layered_assistant.conversations_path, class: "l-ui-button--outline" %>
4
- </div>
1
+ <%= l_ui_title_bar(
2
+ title: "Messages",
3
+ breadcrumbs: [["Conversations", layered_assistant.conversations_path]]
4
+ ) %>
5
5
 
6
6
  <div class="l-ui-container--table l-ui-utility--mt-lg">
7
7
  <table class="l-ui-table">
@@ -1,6 +1,9 @@
1
- <div class="l-ui-container--spread">
2
- <h1>Edit Model</h1>
3
- <%= link_to "Back", layered_assistant.provider_models_path(@provider), class: "l-ui-button--outline" %>
4
- </div>
1
+ <%= l_ui_title_bar(
2
+ title: "Edit model",
3
+ breadcrumbs: [
4
+ ["Providers", layered_assistant.providers_path],
5
+ ["Models", layered_assistant.provider_models_path(@provider)]
6
+ ]
7
+ ) %>
5
8
 
6
9
  <%= render "form", model: @model, url: layered_assistant.provider_model_path(@provider, @model) %>
@@ -1,10 +1,9 @@
1
- <div class="l-ui-container--spread">
2
- <h1>Models</h1>
3
- <div class="l-ui-container--spread">
4
- <%= link_to "Back", layered_assistant.providers_path, class: "l-ui-button--outline" %>
5
- <%= link_to "New", layered_assistant.new_provider_model_path(@provider), class: "l-ui-button--primary" %>
6
- </div>
7
- </div>
1
+ <%= l_ui_title_bar(
2
+ title: "Models",
3
+ breadcrumbs: [["Providers", layered_assistant.providers_path]]
4
+ ) do %>
5
+ <%= link_to "New", layered_assistant.new_provider_model_path(@provider), class: "l-ui-button--primary" %>
6
+ <% end %>
8
7
 
9
8
  <div class="l-ui-container--table l-ui-utility--mt-lg">
10
9
  <table class="l-ui-table">
@@ -1,6 +1,9 @@
1
- <div class="l-ui-container--spread">
2
- <h1>New Model</h1>
3
- <%= link_to "Back", layered_assistant.provider_models_path(@provider), class: "l-ui-button--outline" %>
4
- </div>
1
+ <%= l_ui_title_bar(
2
+ title: "New model",
3
+ breadcrumbs: [
4
+ ["Providers", layered_assistant.providers_path],
5
+ ["Models", layered_assistant.provider_models_path(@provider)]
6
+ ]
7
+ ) %>
5
8
 
6
9
  <%= render "form", model: @model, url: layered_assistant.provider_models_path(@provider) %>
@@ -1,2 +1,6 @@
1
- <h1>Edit persona</h1>
1
+ <%= l_ui_title_bar(
2
+ title: "Edit persona",
3
+ breadcrumbs: [["Personas", layered_assistant.personas_path]]
4
+ ) %>
5
+
2
6
  <%= render "form", persona: @persona, url: layered_assistant.persona_path(@persona) %>
@@ -1,7 +1,6 @@
1
- <div class="l-ui-container--spread">
2
- <h1>Personas</h1>
1
+ <%= l_ui_title_bar(title: "Personas") do %>
3
2
  <%= link_to "New", layered_assistant.new_persona_path, class: "l-ui-button--primary" %>
4
- </div>
3
+ <% end %>
5
4
 
6
5
  <div class="l-ui-container--table l-ui-utility--mt-lg">
7
6
  <table class="l-ui-table">
@@ -1,2 +1,6 @@
1
- <h1>New persona</h1>
1
+ <%= l_ui_title_bar(
2
+ title: "New persona",
3
+ breadcrumbs: [["Personas", layered_assistant.personas_path]]
4
+ ) %>
5
+
2
6
  <%= render "form", persona: @persona, url: layered_assistant.personas_path %>
@@ -1,6 +1,6 @@
1
- <div class="l-ui-container--spread">
2
- <h1>Edit Provider</h1>
3
- <%= link_to "Back", layered_assistant.providers_path, class: "l-ui-button--outline" %>
4
- </div>
1
+ <%= l_ui_title_bar(
2
+ title: "Edit provider",
3
+ breadcrumbs: [["Providers", layered_assistant.providers_path]]
4
+ ) %>
5
5
 
6
6
  <%= render "form", provider: @provider, url: layered_assistant.provider_path(@provider) %>
@@ -1,7 +1,6 @@
1
- <div class="l-ui-container--spread">
2
- <h1>Providers</h1>
1
+ <%= l_ui_title_bar(title: "Providers") do %>
3
2
  <%= link_to "New", layered_assistant.new_provider_path, class: "l-ui-button--primary" %>
4
- </div>
3
+ <% end %>
5
4
 
6
5
  <div class="l-ui-container--table l-ui-utility--mt-lg">
7
6
  <table class="l-ui-table">
@@ -1,6 +1,6 @@
1
- <div class="l-ui-container--spread">
2
- <h1>New Provider</h1>
3
- <%= link_to "Back", layered_assistant.providers_path, class: "l-ui-button--outline" %>
4
- </div>
1
+ <%= l_ui_title_bar(
2
+ title: "New provider",
3
+ breadcrumbs: [["Providers", layered_assistant.providers_path]]
4
+ ) %>
5
5
 
6
6
  <%= render "form", provider: @provider, url: layered_assistant.providers_path %>
@@ -1,4 +1,4 @@
1
- <h1>Assistants</h1>
1
+ <%= l_ui_title_bar(title: "Assistants") %>
2
2
 
3
3
  <% if @assistants.any? %>
4
4
  <div class="l-ui-container--table l-ui-utility--mt-lg">
@@ -1,2 +1,3 @@
1
- <h1>Setup</h1>
1
+ <%= l_ui_title_bar(title: "Setup") %>
2
+
2
3
  <%= render "layered/assistant/setup/setup" %>
@@ -1,2 +1,6 @@
1
- <h1>Edit skill</h1>
1
+ <%= l_ui_title_bar(
2
+ title: "Edit skill",
3
+ breadcrumbs: [["Skills", layered_assistant.skills_path]]
4
+ ) %>
5
+
2
6
  <%= render "form", skill: @skill, url: layered_assistant.skill_path(@skill) %>
@@ -1,7 +1,6 @@
1
- <div class="l-ui-container--spread">
2
- <h1>Skills</h1>
1
+ <%= l_ui_title_bar(title: "Skills") do %>
3
2
  <%= link_to "New", layered_assistant.new_skill_path, class: "l-ui-button--primary" %>
4
- </div>
3
+ <% end %>
5
4
 
6
5
  <div class="l-ui-container--table l-ui-utility--mt-lg">
7
6
  <table class="l-ui-table">
@@ -1,2 +1,6 @@
1
- <h1>New skill</h1>
1
+ <%= l_ui_title_bar(
2
+ title: "New skill",
3
+ breadcrumbs: [["Skills", layered_assistant.skills_path]]
4
+ ) %>
5
+
2
6
  <%= render "form", skill: @skill, url: layered_assistant.skills_path %>
@@ -7,14 +7,18 @@
7
7
  <% content_for :l_ui_navigation_items do %>
8
8
  <%= render "layouts/layered/assistant/host_navigation" %>
9
9
  <% if l_assistant_accessible? %>
10
- <%= l_ui_navigation_item "Setup", layered_assistant.root_path %>
11
- <%= l_ui_navigation_item "Providers", layered_assistant.providers_path %>
12
- <%= l_ui_navigation_item "Personas", layered_assistant.personas_path %>
13
- <%= l_ui_navigation_item "Skills", layered_assistant.skills_path %>
14
- <%= l_ui_navigation_item "Assistants", layered_assistant.assistants_path %>
15
- <%= l_ui_navigation_item "Conversations", layered_assistant.conversations_path %>
10
+ <%= l_ui_navigation_section do %>
11
+ <%= l_ui_navigation_item "Setup", layered_assistant.root_path, icon_path: "layered_assistant/icon_spanner.svg" %>
12
+ <%= l_ui_navigation_item "Providers", layered_assistant.providers_path, icon_path: "layered_assistant/icon_providers.svg" %>
13
+ <%= l_ui_navigation_item "Personas", layered_assistant.personas_path, icon_path: "layered_assistant/icon_personas.svg" %>
14
+ <%= l_ui_navigation_item "Skills", layered_assistant.skills_path, icon_path: "layered_assistant/icon_skills.svg" %>
15
+ <%= l_ui_navigation_item "Assistants", layered_assistant.assistants_path, icon_path: "layered_assistant/icon_assistants.svg" %>
16
+ <%= l_ui_navigation_item "Conversations", layered_assistant.conversations_path, icon_path: "layered_assistant/icon_conversations.svg" %>
17
+ <% end %>
16
18
  <% else %>
17
- <%= l_ui_navigation_item "Assistants", layered_assistant.public_assistants_path %>
19
+ <%= l_ui_navigation_section do %>
20
+ <%= l_ui_navigation_item "Assistants", layered_assistant.public_assistants_path, icon_path: "layered_assistant/icon_assistants.svg" %>
21
+ <% end %>
18
22
  <% end %>
19
23
  <% end %>
20
24
 
@@ -0,0 +1,26 @@
1
+ module Layered
2
+ module Assistant
3
+ module Generators
4
+ class InstallAgentSkillGenerator < Rails::Generators::Base
5
+ desc "Copy the layered-assistant-rails agent skill into the host application"
6
+
7
+ def self.source_root
8
+ Layered::Assistant::Engine.root
9
+ end
10
+
11
+ def copy_skill
12
+ skill_source = File.join(self.class.source_root, ".claude/skills/layered-assistant-rails")
13
+ skill_dest = ".claude/skills/layered-assistant-rails"
14
+
15
+ directory skill_source, skill_dest
16
+ end
17
+
18
+ def show_instructions
19
+ say ""
20
+ say "Agent skill installed to .claude/skills/layered-assistant-rails/", :green
21
+ say ""
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -21,38 +21,6 @@ module Layered
21
21
  end
22
22
  end
23
23
 
24
- def copy_css
25
- source_path = File.join(self.class.source_root, "app/assets/tailwind/layered/assistant/styles.css")
26
- source_content = File.read(source_path)
27
-
28
- header = <<~CSS
29
- /*
30
- * layered-assistant-rails v#{Layered::Assistant::VERSION}
31
- *
32
- * This file was automatically generated by the layered:assistant:install generator.
33
- * Do not modify directly. To update, re-run: bin/rails generate layered:assistant:install
34
- */
35
-
36
- CSS
37
-
38
- create_file "app/assets/tailwind/layered_assistant.css", header + source_content, force: true
39
- end
40
-
41
- def add_css_import
42
- application_css = "app/assets/tailwind/application.css"
43
-
44
- return unless File.exist?(application_css)
45
-
46
- content = File.read(application_css)
47
- import_line = '@import "./layered_assistant";'
48
-
49
- return if content.match?(%r{@import\s+['"]\.?/?layered_assistant['"]})
50
-
51
- # Insert after the layered_ui import (which must already be present)
52
- inject_into_file application_css, "\n#{import_line}", after: %r{@import\s+['"]\.?/?layered_ui['"];?}
53
- say "Added import to #{application_css}", :green
54
- end
55
-
56
24
  def add_js_import
57
25
  application_js = "app/javascript/application.js"
58
26
 
@@ -15,6 +15,7 @@ module Layered
15
15
 
16
16
  initializer "layered-assistant-rails.assets" do |app|
17
17
  app.config.assets.paths << Engine.root.join("app/javascript")
18
+ app.config.assets.paths << Engine.root.join("app/assets/images")
18
19
  end
19
20
 
20
21
  initializer "layered-assistant-rails.helpers" do
@@ -1,5 +1,5 @@
1
1
  module Layered
2
2
  module Assistant
3
- VERSION = "0.4.0"
3
+ VERSION = "0.5.0"
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: layered-assistant-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - layered.ai
@@ -113,14 +113,14 @@ dependencies:
113
113
  requirements:
114
114
  - - "~>"
115
115
  - !ruby/object:Gem::Version
116
- version: '0.8'
116
+ version: '0.10'
117
117
  type: :runtime
118
118
  prerelease: false
119
119
  version_requirements: !ruby/object:Gem::Requirement
120
120
  requirements:
121
121
  - - "~>"
122
122
  - !ruby/object:Gem::Version
123
- version: '0.8'
123
+ version: '0.10'
124
124
  - !ruby/object:Gem::Dependency
125
125
  name: propshaft
126
126
  requirement: !ruby/object:Gem::Requirement
@@ -297,12 +297,18 @@ executables: []
297
297
  extensions: []
298
298
  extra_rdoc_files: []
299
299
  files:
300
+ - ".claude/skills/layered-assistant-rails/SKILL.md"
300
301
  - AGENTS.md
301
302
  - LICENSE
302
303
  - NOTICE
303
304
  - README.md
304
305
  - Rakefile
305
- - app/assets/tailwind/layered/assistant/styles.css
306
+ - app/assets/images/layered_assistant/icon_assistants.svg
307
+ - app/assets/images/layered_assistant/icon_conversations.svg
308
+ - app/assets/images/layered_assistant/icon_personas.svg
309
+ - app/assets/images/layered_assistant/icon_providers.svg
310
+ - app/assets/images/layered_assistant/icon_skills.svg
311
+ - app/assets/images/layered_assistant/icon_spanner.svg
306
312
  - app/controllers/concerns/layered/assistant/message_creation.rb
307
313
  - app/controllers/concerns/layered/assistant/public/session_conversations.rb
308
314
  - app/controllers/concerns/layered/assistant/stoppable_response.rb
@@ -419,6 +425,7 @@ files:
419
425
  - db/migrate/20260406000000_rename_system_prompt_to_instructions.rb
420
426
  - db/migrate/20260406000001_create_layered_assistant_skills.rb
421
427
  - db/migrate/20260406000002_create_layered_assistant_assistant_skills.rb
428
+ - lib/generators/layered/assistant/install_agent_skill_generator.rb
422
429
  - lib/generators/layered/assistant/install_generator.rb
423
430
  - lib/generators/layered/assistant/migrations_generator.rb
424
431
  - lib/generators/layered/assistant/templates/initializer.rb
@@ -432,18 +439,20 @@ licenses:
432
439
  metadata:
433
440
  homepage_uri: https://www.layered.ai
434
441
  source_code_uri: https://github.com/layered-ai-public/layered-assistant-rails
435
- changelog_uri: https://github.com/layered-ai-public/layered-assistant-rails/blob/main/CHANGELOG.md
436
442
  bug_tracker_uri: https://github.com/layered-ai-public/layered-assistant-rails/issues
443
+ changelog_uri: https://github.com/layered-ai-public/layered-assistant-rails/blob/main/CHANGELOG.md
444
+ documentation_uri: https://layered-assistant-rails.layered.ai/
445
+ discord_uri: https://discord.gg/aCGqz9Bx
446
+ rubygems_mfa_required: 'true'
437
447
  post_install_message: |
438
448
  To complete installation, run:
439
449
 
440
450
  bin/rails generate layered:assistant:install
441
451
 
442
452
  This command will:
443
- • Copy the layered assistant CSS to your host app at app/assets/tailwind/layered_assistant.css
444
- • This approach ensures the CSS is processed with your host app's Tailwind configuration
445
- • Add an import statement to your app/assets/tailwind/application.css
446
453
  • Add `import "layered_assistant"` to your app/javascript/application.js (just after `import "layered_ui"`, which must already be present)
454
+ • Mount the engine at /layered/assistant in your config/routes.rb
455
+ • Create a starter initialiser at config/initializers/layered_assistant.rb
447
456
  • Copy engine migrations to your app's db/migrate/
448
457
 
449
458
  If these imports already exist, they will not be duplicated.
@@ -459,7 +468,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
459
468
  requirements:
460
469
  - - ">="
461
470
  - !ruby/object:Gem::Version
462
- version: 3.2.0
471
+ version: 3.3.0
463
472
  required_rubygems_version: !ruby/object:Gem::Requirement
464
473
  requirements:
465
474
  - - ">="
@@ -1,13 +0,0 @@
1
- /*
2
- * CSS Formatting Convention
3
- *
4
- * All @apply directives use multi-line formatting with logical grouping:
5
- * 1. Layout (flex, block, fixed, relative, etc.)
6
- * 2. Spacing (padding, margin, gap, width, height)
7
- * 3. Typography (text, font)
8
- * 4. Colors (bg, text, border)
9
- * 5. Effects (hover, transitions, shadows, etc.)
10
- *
11
- * Single-utility classes may remain on one line for brevity.
12
- * This structure improves readability, maintainability, and makes diffs easier to review.
13
- */