ask-rails-harness 0.1.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 +7 -0
  2. data/CHANGELOG.md +24 -0
  3. data/LICENSE +21 -0
  4. data/README.md +122 -0
  5. data/app/controllers/ask/rails/harness/chat_controller.rb +310 -0
  6. data/app/models/ask/rails/harness/session.rb +25 -0
  7. data/app/views/ask/rails/harness/chat/index.html.erb +6 -0
  8. data/app/views/layouts/ask/rails/harness/application.html.erb +521 -0
  9. data/config/routes.rb +17 -0
  10. data/lib/ask/rails/harness/audit_log.rb +183 -0
  11. data/lib/ask/rails/harness/auth.rb +24 -0
  12. data/lib/ask/rails/harness/configuration.rb +73 -0
  13. data/lib/ask/rails/harness/engine.rb +13 -0
  14. data/lib/ask/rails/harness/environment_permissions.rb +41 -0
  15. data/lib/ask/rails/harness/persistence.rb +54 -0
  16. data/lib/ask/rails/harness/railtie.rb +34 -0
  17. data/lib/ask/rails/harness/service_discovery.rb +50 -0
  18. data/lib/ask/rails/harness/tool.rb +42 -0
  19. data/lib/ask/rails/harness/tools/query_database.rb +74 -0
  20. data/lib/ask/rails/harness/tools/read_file.rb +23 -0
  21. data/lib/ask/rails/harness/tools/read_log.rb +120 -0
  22. data/lib/ask/rails/harness/tools/read_model.rb +75 -0
  23. data/lib/ask/rails/harness/tools/read_routes.rb +20 -0
  24. data/lib/ask/rails/harness/tools/route_inspector.rb +71 -0
  25. data/lib/ask/rails/harness/tools/run_command.rb +60 -0
  26. data/lib/ask/rails/harness/tools/schema_graph.rb +176 -0
  27. data/lib/ask/rails/harness/tools/search_codebase.rb +23 -0
  28. data/lib/ask/rails/harness/version.rb +9 -0
  29. data/lib/ask/rails/harness.rb +189 -0
  30. data/lib/ask/skills/rails.db_debug/SKILL.md +118 -0
  31. data/lib/ask/skills/rails.deploy_pipeline/SKILL.md +125 -0
  32. data/lib/ask/skills/rails.route_trouble/SKILL.md +127 -0
  33. data/lib/ask-rails-harness.rb +3 -0
  34. data/lib/generators/ask/rails/harness/install/install_generator.rb +36 -0
  35. data/lib/generators/ask/rails/harness/install/templates/audit_log_migration.rb +22 -0
  36. data/lib/generators/ask/rails/harness/install/templates/initializer.rb +5 -0
  37. data/lib/generators/ask/rails/harness/install/templates/migration.rb +12 -0
  38. metadata +208 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 1c1a8388500ab4366c876be18f2b8fb2fd2c489fcd6c04da90c2a1ee9eea8b59
4
+ data.tar.gz: 6012eff1acc1cdecd8b606a91027f517a69ddb77d252c0aae8846e23b5e7f15e
5
+ SHA512:
6
+ metadata.gz: 7492561a0888730defddcff380d3800afefab364f41bf4df2e842a1894cdc4229c08e7d9c249df0b2d8ece1d0e9e345369840434a1af10d8802eb2f9f2fac4c4
7
+ data.tar.gz: 490b772093ac41af1ca679dfa55af7500ede1c634d01e82a3f7393ba23381fdf30507bf95929d8405e824ab7daf0d05f17052164a41724e8547bf7c4f3848abb
data/CHANGELOG.md ADDED
@@ -0,0 +1,24 @@
1
+ ## [0.1.0] — 2026-07-25
2
+
3
+ ### Changed
4
+
5
+ - **Gem renamed from `ask-rails` to `ask-rails-harness`** — The gem has been renamed to better reflect its purpose as an admin AI copilot harness for Rails apps. The old `ask-rails` gem name has been yanked from rubygems.org to make room for a new `ask-rails` gem with a different purpose.
6
+ - **Module namespace moved from `Ask::Rails` to `Ask::Rails::Harness`** — All code now lives under `Ask::Rails::Harness::*` to coexist with the planned `ask-rails` gem.
7
+ - **Engine namespace changed** — `isolate_namespace` updated from `Ask::Rails` to `Ask::Rails::Harness`. Route helpers change from `ask_rails.*` to `ask_rails_harness.*`.
8
+ - **Generator renamed** — Use `rails generate ask_rails_harness:install` instead of `ask_rails:install`.
9
+ - **Initializer renamed** — Generated file is now `config/initializers/ask_rails_harness.rb`.
10
+ - **Rake task renamed** — Use `rails ask_rails_harness:cleanup` instead of `ask_rails:cleanup`.
11
+ - **Notification name changed** — `audit_log.ask_rails` is now `audit_log.ask_rails_harness`.
12
+ - **Log prefix changed** — `[ask-rails]` is now `[ask-rails-harness]`.
13
+
14
+ ### Migration from ask-rails
15
+
16
+ Existing applications using `ask-rails` should:
17
+ 1. Replace `gem "ask-rails"` with `gem "ask-rails-harness"` in your Gemfile
18
+ 2. Update `Ask::Rails.configure` → `Ask::Rails::Harness.configure`
19
+ 3. Update `mount Ask::Rails::Engine` → `mount Ask::Rails::Harness::Engine`
20
+ 4. Update `ask_rails.*` route helpers → `ask_rails_harness.*`
21
+ 5. Update `Ask::Rails::Auth` → `Ask::Rails::Harness::Auth`
22
+ 6. Rename initializer from `ask_rails.rb` to `ask_rails_harness.rb`
23
+
24
+ All previous versions (0.1.0–0.11.1) were released under the `ask-rails` gem name. See the [ask-rails CHANGELOG](https://github.com/ask-rb/ask-rails) for history of those releases.
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kaka Ruto
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,122 @@
1
+ # ask-rails-harness
2
+
3
+ An admin AI agent for your Rails app. Mount the engine, get a chat interface at `/ask` that can inspect your code, query your database, read logs, and help you debug — all through an authenticated admin UI.
4
+
5
+ > **Previously developed as `ask-rails` (v0.1.0–0.11.1).** Renamed to `ask-rails-harness` for clarity — this gem is the *harness* that wraps an AI agent with Rails-aware tools, environment, state, and feedback for internal/admin use.
6
+
7
+ ## Who is this for?
8
+
9
+ - **Rails developers** who want an AI co-pilot that understands their app's codebase, schema, routes, and logs
10
+ - **Internal/Admin use only** — the agent has direct access to your database, file system, and shell. Not for external/customer-facing use.
11
+
12
+ For building customer-facing AI agents, use `ask-agent` directly with your own tools and UI.
13
+
14
+ ## What it gives you
15
+
16
+ - **9 Rails-aware tools**: `ReadFile`, `QueryDatabase`, `ReadRoutes`, `ReadModel`, `ReadLog`, `RunCommand`, `SearchCodebase`, `SchemaGraph`, `RouteInspector`
17
+ - **Admin chat UI**: Mount the engine, get a working chat at `/ask` with SSE streaming
18
+ - **Auth integration**: Protect `/ask` behind your existing Devise/authentication
19
+ - **AR persistence**: Agent sessions survive server restarts
20
+ - **Service discovery**: Auto-detects installed ask-\* service gems
21
+ - **Skills**: Built-in guides for Rails debugging, deployment, and database performance
22
+
23
+ ## Installation
24
+
25
+ ```bash
26
+ bundle add ask-rails-harness
27
+ rails generate ask_rails_harness:install
28
+ ```
29
+
30
+ ## Quick Start
31
+
32
+ Add the engine mount and auth protection to `config/routes.rb`:
33
+
34
+ ```ruby
35
+ # config/routes.rb
36
+ Rails.application.routes.draw do
37
+ # ... your routes ...
38
+
39
+ authenticate :user, ->(u) { u.admin? } do
40
+ mount Ask::Rails::Harness::Engine, at: "/ask"
41
+ end
42
+ end
43
+ ```
44
+
45
+ Then visit `/ask` in your browser.
46
+
47
+ ## Usage
48
+
49
+ ### Configuration
50
+
51
+ ```ruby
52
+ # config/initializers/ask_rails_harness.rb
53
+ Ask::Rails::Harness.configure do |c|
54
+ c.default_model = "claude-sonnet-4"
55
+ c.max_turns = 50
56
+ end
57
+ ```
58
+
59
+ ### Programmatic Access
60
+
61
+ ```ruby
62
+ # From any controller, view, or job
63
+ session = Ask::Rails::Harness.agent_session
64
+ session.run("Find all open issues labeled 'bug' in our repo")
65
+ ```
66
+
67
+ ### Route Helpers
68
+
69
+ ```ruby
70
+ ask_rails_harness.root_path # => /ask
71
+ ask_rails_harness.sessions_path # => /ask/sessions
72
+ ask_rails_harness.session_messages_path(session_id) # => /ask/sessions/:id/messages
73
+ ```
74
+
75
+ ### Auth
76
+
77
+ By default, the admin chat is unprotected. Add auth in your routes (as shown above) or set a custom check:
78
+
79
+ ```ruby
80
+ # config/initializers/ask_rails_harness.rb
81
+ Ask::Rails::Harness::Auth.check = -> {
82
+ redirect_to main_app.login_path unless current_user&.admin?
83
+ }
84
+ ```
85
+
86
+ ## Tools
87
+
88
+ | Tool | What it does |
89
+ |---|---|
90
+ | `ReadFile` | Read any file (relative to `Rails.root`) |
91
+ | `QueryDatabase` | Run read-only SQL (rejects non-SELECT in production) |
92
+ | `ReadModel` | Inspect AR model schema, associations, validations |
93
+ | `ReadRoutes` | View `config/routes.rb` |
94
+ | `ReadLog` | Read log files with level/search filtering |
95
+ | `RunCommand` | Run shell commands in the app root |
96
+ | `SearchCodebase` | Grep the codebase for patterns |
97
+ | `SchemaGraph` | Full schema introspection — all models, tables, columns, associations |
98
+ | `RouteInspector` | Parsed route table with filters |
99
+
100
+ ## Engine Routes
101
+
102
+ ```
103
+ GET /ask → Chat UI
104
+ POST /ask/sessions → Create new session
105
+ POST /ask/sessions/:id/messages → Send message (SSE streamed response)
106
+ GET /ask/sessions/:id/messages → Get message history
107
+ GET /ask/sessions/:id/stream → SSE stream for existing session
108
+ ```
109
+
110
+ ## Compared to ask-agent
111
+
112
+ | `ask-agent` | `ask-rails-harness` |
113
+ |---|---|
114
+ | Build external-facing agents | Build an internal admin co-pilot |
115
+ | Bring your own tools | Ships Rails-specific tools |
116
+ | Bring your own UI | Ships an admin chat UI |
117
+ | Any Ruby app | Rails apps only |
118
+ | General purpose | Development, debugging, ops |
119
+
120
+ ## License
121
+
122
+ MIT
@@ -0,0 +1,310 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Ask
6
+ module Rails
7
+ module Harness
8
+ class ChatController < ::ActionController::Base
9
+ layout "ask/rails/harness/application"
10
+
11
+ before_action :authenticate!, unless: -> { Ask::Rails::Harness::Auth.check.nil? }
12
+ before_action :set_session, only: [:message, :stream, :history, :show, :audit]
13
+
14
+ # GET /ask — main chat interface
15
+ def index
16
+ @sessions = load_sessions
17
+ @active_session = @sessions.first
18
+ @audit_logs = load_recent_audit_logs
19
+ render :index
20
+ end
21
+
22
+ # POST /ask/sessions — create a new session
23
+ def create
24
+ session_id = SecureRandom.uuid
25
+
26
+ agent = build_agent_session
27
+ agent.save
28
+
29
+ store_session_metadata(session_id, agent)
30
+
31
+ redirect_to ask_rails_harness.root_path
32
+ end
33
+
34
+ # GET /ask/sessions/:id — show a specific session
35
+ def show
36
+ @messages = load_session_messages(@session_id)
37
+ @audit_logs = load_session_audit_logs(@session_id)
38
+ render json: { id: @session_id, messages: @messages, audit_logs: @audit_logs }
39
+ end
40
+
41
+ # GET /ask/sessions — list all sessions
42
+ def index_sessions
43
+ @sessions = load_sessions
44
+ render json: @sessions
45
+ end
46
+
47
+ # POST /ask/sessions/:session_id/messages — send a message, get SSE stream
48
+ def message
49
+ prompt = params[:message].to_s.strip
50
+ return head :bad_request if prompt.empty?
51
+
52
+ response.headers["Content-Type"] = "text/event-stream"
53
+ response.headers["Cache-Control"] = "no-cache"
54
+ response.headers["X-Accel-Buffering"] = "no"
55
+ response.headers["Last-Modified"] = Time.now.httpdate
56
+
57
+ self.response_body = Enumerator.new do |yielder|
58
+ agent = resume_or_create_session(@session_id)
59
+
60
+ # Subscribe to tool execution events
61
+ tool_sub = subscribe_tool_events(agent, yielder)
62
+
63
+ yielder << "data: #{JSON.generate(type: 'start', session_id: @session_id)}\n\n"
64
+
65
+ begin
66
+ agent.run(prompt) do |chunk|
67
+ if chunk.content&.length&.> 0
68
+ yielder << "data: #{JSON.generate(type: 'delta', content: chunk.content)}\n\n"
69
+ end
70
+ if chunk.thinking&.length&.> 0
71
+ yielder << "data: #{JSON.generate(type: 'thinking', content: chunk.thinking)}\n\n"
72
+ end
73
+ end
74
+
75
+ agent.save
76
+ @session = agent
77
+
78
+ yielder << "data: #{JSON.generate(type: 'done', session_id: @session_id)}\n\n"
79
+ rescue Ask::Agent::MaxTurnsExceeded => e
80
+ yielder << "data: #{JSON.generate(type: 'error', message: "Agent hit the turn limit (#{e.message})")}\n\n"
81
+ rescue Ask::Auth::MissingCredential => e
82
+ yielder << "data: #{JSON.generate(type: 'error', message: "Missing API key: #{e.message}. Check your provider configuration.")}\n\n"
83
+ rescue Ask::Auth::InvalidCredential => e
84
+ yielder << "data: #{JSON.generate(type: 'error', message: "Invalid API key: #{e.message}. Update your credentials.")}\n\n"
85
+ rescue StandardError => e
86
+ yielder << "data: #{JSON.generate(type: 'error', message: "Agent error: #{e.message}")}\n\n"
87
+ end
88
+ ensure
89
+ agent&.remove_event_subscriber(tool_sub) if tool_sub
90
+ end
91
+ end
92
+
93
+ # GET /ask/sessions/:session_id/stream — SSE stream for an existing session
94
+ def stream
95
+ response.headers["Content-Type"] = "text/event-stream"
96
+ response.headers["Cache-Control"] = "no-cache"
97
+ response.headers["X-Accel-Buffering"] = "no"
98
+
99
+ self.response_body = Enumerator.new do |yielder|
100
+ messages = load_session_messages(@session_id)
101
+ audit_logs = load_session_audit_logs(@session_id)
102
+ yielder << "data: #{JSON.generate(type: 'history', messages: messages, audit_logs: audit_logs)}\n\n"
103
+
104
+ sleep 30
105
+ yielder << "data: #{JSON.generate(type: 'keepalive')}\n\n"
106
+ end
107
+ end
108
+
109
+ # GET /ask/sessions/:session_id/messages — get message history as JSON
110
+ def history
111
+ messages = load_session_messages(@session_id)
112
+ render json: messages
113
+ end
114
+
115
+ # GET /ask/sessions/:session_id/audit — get audit logs for a session
116
+ def audit
117
+ logs = load_session_audit_logs(@session_id)
118
+ render json: logs
119
+ end
120
+
121
+ # DELETE /ask/sessions — destroy all sessions
122
+ def destroy_all
123
+ persistence = Ask::Rails::Harness.configuration.persistence_adapter
124
+ if persistence
125
+ persistence.list.each { |id| persistence.delete(id) }
126
+ end
127
+ redirect_to ask_rails_harness.root_path
128
+ end
129
+
130
+ # DELETE /ask/sessions/:session_id — destroy a specific session
131
+ def destroy_session
132
+ persistence = Ask::Rails::Harness.configuration.persistence_adapter
133
+ persistence&.delete(params[:session_id])
134
+ redirect_to ask_rails_harness.root_path
135
+ end
136
+
137
+ private
138
+
139
+ def authenticate!
140
+ instance_eval(&Ask::Rails::Harness::Auth.check) if Ask::Rails::Harness::Auth.check
141
+ end
142
+
143
+ def set_session
144
+ @session_id = params[:session_id] || params[:id]
145
+ head :not_found unless @session_id
146
+ end
147
+
148
+ def build_agent_session(**extra)
149
+ Ask::Rails::Harness.agent_session(**extra)
150
+ end
151
+
152
+ def subscribe_tool_events(agent, yielder)
153
+ return nil unless agent.respond_to?(:on_event)
154
+
155
+ agent.on_event do |event|
156
+ case event
157
+ when Ask::Agent::Events::ToolExecutionStart
158
+ yielder << "data: #{JSON.generate(
159
+ type: 'tool_start',
160
+ name: event.name,
161
+ id: event.id,
162
+ args: safe_tool_args(event.arguments)
163
+ )}\n\n"
164
+ when Ask::Agent::Events::ToolExecutionEnd
165
+ yielder << "data: #{JSON.generate(
166
+ type: 'tool_end',
167
+ name: event.name,
168
+ id: event.id,
169
+ duration_ms: event.duration_ms,
170
+ is_error: event.is_error
171
+ )}\n\n"
172
+ when Ask::Agent::Events::ToolExecutionUpdate
173
+ yielder << "data: #{JSON.generate(
174
+ type: 'tool_update',
175
+ id: event.id,
176
+ content: event.partial_result.to_s.truncate(200)
177
+ )}\n\n"
178
+ end
179
+ end
180
+ end
181
+
182
+ def safe_tool_args(args)
183
+ return {} unless args.is_a?(Hash)
184
+
185
+ safe = args.dup
186
+ %w[password secret token api_key key auth_token access_token sql command].each do |sensitive|
187
+ safe[sensitive] = "[REDACTED]" if safe.key?(sensitive)
188
+ end
189
+ safe
190
+ end
191
+
192
+ def resume_or_create_session(session_id)
193
+ persistence = Ask::Rails::Harness.configuration.persistence_adapter
194
+
195
+ if persistence
196
+ data = persistence.load(session_id)
197
+ if data
198
+ session = Ask::Agent::Session.load(session_id, adapter: persistence)
199
+ return session if session
200
+ end
201
+ end
202
+
203
+ Ask::Rails::Harness.agent_session(persistence: persistence).tap do |s|
204
+ s.instance_variable_set(:@id, session_id)
205
+ end
206
+ end
207
+
208
+ def load_sessions
209
+ persistence = Ask::Rails::Harness.configuration.persistence_adapter
210
+ return [] unless persistence
211
+
212
+ persistence.list.map do |id|
213
+ data = persistence.load(id) || {}
214
+ messages = data[:messages] || []
215
+ { id: id, created_at: data.dig(:metadata, :created_at), message_count: messages.length, preview: messages.first&.dig(:content).to_s.truncate(60) }
216
+ end.sort_by { |s| s[:created_at] || "" }.reverse
217
+ end
218
+
219
+ def load_session_messages(session_id)
220
+ persistence = Ask::Rails::Harness.configuration.persistence_adapter
221
+ return [] unless persistence
222
+
223
+ data = persistence.load(session_id)
224
+ return [] unless data
225
+
226
+ (data[:messages] || []).map { |m|
227
+ { role: m[:role], content: m[:content].to_s }
228
+ }
229
+ end
230
+
231
+ def load_session_audit_logs(session_id)
232
+ return [] unless defined?(ActiveRecord::Base)
233
+
234
+ ActiveRecord::Base.connection.execute(
235
+ "SELECT tool_name, status, duration_ms, environment, recorded_at, " \
236
+ "params, result_summary, error_message, user_context " \
237
+ "FROM ask_audit_logs WHERE session_id = #{ActiveRecord::Base.connection.quote(session_id.to_s)} " \
238
+ "ORDER BY recorded_at ASC"
239
+ ).map do |row|
240
+ {
241
+ tool_name: row["tool_name"],
242
+ status: row["status"],
243
+ duration_ms: row["duration_ms"],
244
+ environment: row["environment"],
245
+ recorded_at: row["recorded_at"],
246
+ params: parse_json_field(row["params"]),
247
+ result_summary: parse_json_field(row["result_summary"]),
248
+ error_message: row["error_message"],
249
+ user_context: parse_json_field(row["user_context"])
250
+ }
251
+ end
252
+ rescue StandardError
253
+ []
254
+ end
255
+
256
+ def load_recent_audit_logs
257
+ return [] unless defined?(ActiveRecord::Base)
258
+
259
+ ActiveRecord::Base.connection.execute(
260
+ "SELECT session_id, tool_name, status, duration_ms, recorded_at " \
261
+ "FROM ask_audit_logs ORDER BY recorded_at DESC LIMIT 100"
262
+ ).map do |row|
263
+ {
264
+ session_id: row["session_id"].to_s[0..7],
265
+ tool_name: row["tool_name"],
266
+ status: row["status"],
267
+ duration_ms: row["duration_ms"],
268
+ recorded_at: row["recorded_at"]
269
+ }
270
+ end
271
+ rescue StandardError
272
+ []
273
+ end
274
+
275
+ def parse_json_field(value)
276
+ return nil unless value
277
+ return value if value.is_a?(Hash) || value.is_a?(Array)
278
+ JSON.parse(value)
279
+ rescue JSON::ParserError
280
+ value
281
+ end
282
+
283
+ def store_session_metadata(session_id, agent)
284
+ persistence = Ask::Rails::Harness.configuration.persistence_adapter
285
+ return unless persistence
286
+
287
+ persistence.save(session_id, {
288
+ id: session_id,
289
+ messages: agent.messages.map { |m|
290
+ { role: m.role, content: m.content.to_s }
291
+ },
292
+ metadata: {
293
+ model: ask_rails_harness_model,
294
+ created_at: Time.now.iso8601,
295
+ updated_at: Time.now.iso8601
296
+ }
297
+ })
298
+ end
299
+
300
+ def ask_rails_harness_model
301
+ Ask::Rails::Harness.configuration.default_model
302
+ end
303
+
304
+ def tool_events_supported?
305
+ defined?(Ask::Agent::Events::ToolExecutionStart)
306
+ end
307
+ end
308
+ end
309
+ end
310
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ask
4
+ module Rails
5
+ module Harness
6
+ # ActiveRecord model for persisting agent sessions.
7
+ #
8
+ # Created by the +ask_rails_harness:install+ generator. The +ask_sessions+ table
9
+ # stores session state in a JSONB +data+ column, keyed by a unique
10
+ # +session_id+ string.
11
+ #
12
+ # Used automatically when +persistence_adapter+ is configured with the
13
+ # default model class:
14
+ #
15
+ # Ask::Rails::Harness.configure do |config|
16
+ # config.persistence_adapter = Ask::Rails::Harness::Persistence.new
17
+ # end
18
+ #
19
+ # Without +model_class:+, +Persistence+ defaults to +Ask::Rails::Harness::Session+.
20
+ class Session < ActiveRecord::Base
21
+ self.table_name = "ask_sessions"
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,6 @@
1
+ <!-- Chat interface rendered by the layout -->
2
+ <!-- The layout handles the full UI. This view is minimal -->
3
+ <!-- since the layout and JavaScript drive the interface. -->
4
+
5
+ <%= tag.div id: "ask-rails-harness-data",
6
+ data: { sessions: @sessions.to_json, active_session_id: @active_session&.dig(:id) } %>