console_agent 0.1.0 → 0.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '09b1eab5b0fd84fb27fe9af623c234da290779f84d5972d4a67bfe000d581ab2'
4
- data.tar.gz: 344f3afbc67adc4d87c5c2fb4e11ccc902fca071776a4a3aa6cb160783e22998
3
+ metadata.gz: 661ab1a997f36d2b10ea9649aa41367aca039c859c3e2afa01c3cbf8b8dd9345
4
+ data.tar.gz: 5cfe513d78a1785b7199fb0ebbbc70e825092e49e4051caf390e3ccb7ccf23c4
5
5
  SHA512:
6
- metadata.gz: 73a5c9bd000e30b3cfa63c06efbb0f9619849ec56ca8b4b8f1c71cd7eae29439f9b4ef8d10725950aa8fb813159674820706fa5883784060464f8c9959c914aa
7
- data.tar.gz: 4498692aab0860b717b7d309e252cb1b2130b7946a55421f0edd76c3b3469da27c6f8426667e23cf0a2fcab66a356896573f19b819c8ba603f08f50a5e60647d
6
+ metadata.gz: 0d7fd63a81886c7abbf4f82db677b6b4bb9151760c378901306e789b3619d7ca31b743f7f4a14a9a28335e775877243b0c4ff8b8bd6cc6acee0c8a02c270eccc
7
+ data.tar.gz: 3e0308bd89e0421a4a29dbe8c7b7be6afd584a504559a456f830e1c9a268517d131d6dcf00417347f5f3ea80a434bf9211e0515771fc3e43686f0117bf401f9b
data/README.md CHANGED
@@ -1,48 +1,40 @@
1
1
  # ConsoleAgent
2
2
 
3
- An AI-powered assistant for your Rails console. Ask questions in plain English, get executable Ruby code.
3
+ Claude Code, embedded in your Rails console.
4
4
 
5
- ConsoleAgent connects your `rails console` to an LLM (Claude or GPT) and gives it tools to introspect your app's database schema, models, and source code. It figures out what it needs on its own, so you don't pay for 50K tokens of context on every query.
5
+ Ask questions in plain English. The AI explores your schema, models, and source code on its own, then writes and runs the Ruby code for you.
6
6
 
7
- ## Quick Start
8
-
9
- Add to your Gemfile:
7
+ ## Install
10
8
 
11
9
  ```ruby
10
+ # Gemfile
12
11
  gem 'console_agent', group: :development
13
12
  ```
14
13
 
15
- Run the install generator:
16
-
17
14
  ```bash
18
15
  bundle install
19
16
  rails generate console_agent:install
20
17
  ```
21
18
 
22
- Set your API key and open a console:
19
+ Set your API key in the generated initializer or as an env var (`ANTHROPIC_API_KEY`):
23
20
 
24
- ```bash
25
- export ANTHROPIC_API_KEY=sk-ant-...
26
- rails console
21
+ ```ruby
22
+ # config/initializers/console_agent.rb
23
+ ConsoleAgent.configure do |config|
24
+ config.api_key = 'sk-ant-...'
25
+ end
27
26
  ```
28
27
 
29
- Then:
28
+ To set up session logging (OPTIONAL), create the table from the console:
30
29
 
31
30
  ```ruby
32
- ai "show me all users who signed up this week"
31
+ ConsoleAgent.setup!
32
+ # => ConsoleAgent: created console_agent_sessions table.
33
33
  ```
34
34
 
35
- ## Console Commands
36
-
37
- | Command | Description |
38
- |---------|-------------|
39
- | `ai "query"` | Ask a question, review generated code, confirm before executing |
40
- | `ai! "query"` | Ask a question and enter interactive mode for follow-ups |
41
- | `ai!` | Enter interactive mode (type `exit` to leave) |
42
- | `ai? "query"` | Explain only — shows code but never executes |
43
- | `ai_status` | Print current configuration summary |
35
+ To reset the table (e.g. after upgrading), run `ConsoleAgent.teardown!` then `ConsoleAgent.setup!`.
44
36
 
45
- ### Example Session
37
+ ## Usage
46
38
 
47
39
  ```
48
40
  irb> ai "find the 5 most recent orders over $100"
@@ -51,184 +43,178 @@ irb> ai "find the 5 most recent orders over $100"
51
43
  12 tables: users, orders, line_items, products...
52
44
  -> describe_table("orders")
53
45
  8 columns
54
- -> describe_model("Order")
55
- 4 associations, 2 validations
56
-
57
- You can query recent high-value orders like this:
58
46
 
59
47
  Order.where("total > ?", 100).order(created_at: :desc).limit(5)
60
48
 
61
- [tokens in: 1,240 | out: 85 | total: 1,325]
62
49
  Execute? [y/N/edit] y
63
50
  => [#<Order id: 4821, ...>, ...]
64
51
  ```
65
52
 
66
- ### Interactive Mode
53
+ The AI calls tools behind the scenes to learn your app — schema, models, associations, source code — so it writes accurate queries without you providing any context.
54
+
55
+ ### Commands
56
+
57
+ | Command | What it does |
58
+ |---------|-------------|
59
+ | `ai "query"` | One-shot: ask, review code, confirm |
60
+ | `ai! "query"` | Interactive: ask and keep chatting |
61
+ | `ai? "query"` | Explain only, never executes |
62
+
63
+ ### Multi-Step Plans
64
+
65
+ For complex tasks, the AI builds a plan and executes it step by step:
67
66
 
68
67
  ```
69
- irb> ai!
70
- ConsoleAgent interactive mode. Type 'exit' or 'quit' to leave.
71
- ai> show me all tables
68
+ ai> get the most recent salesforce token and count events via the API
72
69
  Thinking...
73
- -> list_tables
74
- 12 tables: users, orders, line_items, products...
70
+ -> describe_table("oauth2_tokens")
71
+ 28 columns
72
+ -> read_file("lib/salesforce_api.rb")
73
+ 202 lines
74
+
75
+ Plan (2 steps):
76
+ 1. Find the most recent active Salesforce OAuth2 token
77
+ token = Oauth2Token.where(provider: "salesforce", active: true)
78
+ .order(updated_at: :desc).first
79
+ 2. Query event count via SOQL
80
+ api = SalesforceApi.new(step1)
81
+ api.query("SELECT COUNT(Id) FROM Event")
82
+
83
+ Accept plan? [y/N/a(uto)] a
84
+ Step 1/2: Find the most recent active Salesforce OAuth2 token
75
85
  ...
76
- ai> now count orders by status
86
+ => #<Oauth2Token id: 1, provider: "salesforce", ...>
87
+
88
+ Step 2/2: Query event count via SOQL
77
89
  ...
78
- ai> exit
79
- [session totals — in: 3,200 | out: 410 | total: 3,610]
80
- Left ConsoleAgent interactive mode.
90
+ => [{"expr0"=>42}]
81
91
  ```
82
92
 
83
- ### Configuration Status
93
+ Each step's return value is available to later steps as `step1`, `step2`, etc.
84
94
 
85
- ```
86
- irb> ai_status
87
- [ConsoleAgent v0.1.0]
88
- Provider: anthropic
89
- Model: claude-opus-4-6
90
- API key: sk-ant-...a3b4
91
- Context mode: smart
92
- Max tokens: 4096
93
- Temperature: 0.2
94
- Timeout: 30s
95
- Max tool rounds:10
96
- Auto-execute: false
97
- Debug: false
98
- ```
95
+ Plan prompt options:
96
+ - **y** — accept, then confirm each step one at a time
97
+ - **a** — accept and auto-run all steps (stays in manual mode for future queries)
98
+ - **N** — decline; you're asked what to change and the AI revises
99
99
 
100
- ## Configuration
100
+ ### Memories
101
101
 
102
- The install generator creates `config/initializers/console_agent.rb`:
102
+ The AI remembers what it learns about your codebase across sessions:
103
103
 
104
- ```ruby
105
- ConsoleAgent.configure do |config|
106
- # LLM provider: :anthropic or :openai
107
- config.provider = :anthropic
104
+ ```
105
+ ai> how does sharding work?
106
+ -> read_file("config/initializers/sharding.rb")
107
+ -> save_memory("Sharding architecture")
108
+ Memory saved
108
109
 
109
- # API key (or set ANTHROPIC_API_KEY / OPENAI_API_KEY env var)
110
- # config.api_key = 'sk-...'
110
+ This app uses database-per-shard. User.count returns the current shard only.
111
+ ```
111
112
 
112
- # Model override (defaults: claude-opus-4-6 for Anthropic, gpt-5.3-codex for OpenAI)
113
- # config.model = 'claude-opus-4-6'
113
+ Next time, it already knows no re-reading files, fewer tokens.
114
114
 
115
- # Context mode:
116
- # :smart - (default) LLM uses tools to fetch schema/model/code on demand
117
- # :full - sends all schema, models, and routes every time
118
- config.context_mode = :smart
115
+ ### Interactive Mode
119
116
 
120
- # Max tokens for LLM response
121
- config.max_tokens = 4096
117
+ ```
118
+ irb> ai!
119
+ ConsoleAgent interactive mode. Type 'exit' to leave.
120
+ Auto-execute: OFF (Shift-Tab or /auto to toggle)
122
121
 
123
- # Temperature (0.0 - 1.0)
124
- config.temperature = 0.2
122
+ ai> show me all tables
123
+ ...
124
+ ai> count orders by status
125
+ ...
126
+ ai> /auto
127
+ Auto-execute: ON
128
+ ai> delete cancelled orders older than 90 days
129
+ ...
130
+ ai> exit
131
+ ```
125
132
 
126
- # Auto-execute generated code without confirmation
127
- config.auto_execute = false
133
+ Toggle `/auto` to skip confirmation prompts. `/debug` shows raw API traffic. `/usage` shows token stats.
128
134
 
129
- # Max tool-use rounds per query in :smart mode
130
- config.max_tool_rounds = 10
135
+ ### Sessions
131
136
 
132
- # HTTP timeout in seconds
133
- config.timeout = 30
137
+ Sessions are saved automatically when session logging is enabled. You can name, list, and resume them.
134
138
 
135
- # Debug mode: prints full API requests/responses and tool calls
136
- # config.debug = true
137
- end
138
139
  ```
139
-
140
- You can also change settings at runtime in the console:
141
-
142
- ```ruby
143
- ConsoleAgent.configure { |c| c.debug = true }
144
- ConsoleAgent.configure { |c| c.provider = :openai }
145
- ENV['OPENAI_API_KEY'] = 'sk-...'
140
+ ai> /name sf_user_123_calendar
141
+ Session named: sf_user_123_calendar
142
+ ai> exit
143
+ Session #42 saved.
144
+ Resume with: ai_resume "sf_user_123_calendar"
145
+ Left ConsoleAgent interactive mode.
146
146
  ```
147
147
 
148
- ## Context Modes
148
+ List recent sessions:
149
149
 
150
- ### Smart Mode (default)
150
+ ```
151
+ irb> ai_sessions
152
+ [Sessions — showing 3]
151
153
 
152
- The LLM gets a minimal system prompt and uses tools to look up what it needs:
154
+ #42 sf_user_123_calendar find user 123 with calendar issues
155
+ [interactive] 5m ago 2340 tokens
153
156
 
154
- - **list_tables** / **describe_table** — database schema on demand
155
- - **list_models** / **describe_model** — ActiveRecord associations, validations
156
- - **list_files** / **read_file** / **search_code** — browse app source code
157
+ #41 count all active users
158
+ [one_shot] 1h ago 850 tokens
157
159
 
158
- This keeps token usage low (~1-3K per query) even for large apps with hundreds of tables.
160
+ #40 debug_payments explain payment flow
161
+ [interactive] 2h ago 4100 tokens
159
162
 
160
- ### Full Mode
163
+ Use ai_resume(id_or_name) to resume a session.
164
+ ```
161
165
 
162
- Sends the entire database schema, all model details, and a route summary in every request. Simple but expensive for large apps (~50K+ tokens). Useful if you want everything available without tool-call round trips.
166
+ Resume a session by name or ID previous output is replayed, then you continue where you left off:
163
167
 
164
- ```ruby
165
- ConsoleAgent.configure { |c| c.context_mode = :full }
166
168
  ```
169
+ irb> ai_resume "sf_user_123_calendar"
170
+ --- Replaying previous session output ---
171
+ ai> find user 123 with calendar issues
172
+ ...previous output...
173
+ --- End of previous output ---
167
174
 
168
- ## Tools Available in Smart Mode
175
+ ConsoleAgent interactive mode (sf_user_123_calendar). Type 'exit' to leave.
176
+ ai> now check their calendar sync status
177
+ ...
178
+ ```
169
179
 
170
- | Tool | Description |
171
- |------|-------------|
172
- | `list_tables` | All database table names |
173
- | `describe_table` | Columns, types, and indexes for one table |
174
- | `list_models` | All model names with association names |
175
- | `describe_model` | Associations, validations, scopes for one model |
176
- | `list_files` | Ruby files in a directory |
177
- | `read_file` | Read a source file (capped at 200 lines) |
178
- | `search_code` | Grep for a pattern across Ruby files |
180
+ Name or rename a session after the fact:
179
181
 
180
- The LLM decides which tools to call based on your question. You can see the tool calls happening in real time.
182
+ ```
183
+ irb> ai_name 41, "active_user_count"
184
+ Session #41 named: active_user_count
185
+ ```
181
186
 
182
- ## Providers
187
+ Filter sessions by search term:
183
188
 
184
- ### Anthropic (default)
189
+ ```
190
+ irb> ai_sessions 20, search: "salesforce"
191
+ ```
185
192
 
186
- Uses the Claude Messages API. Set `ANTHROPIC_API_KEY` or `config.api_key`.
193
+ If you have an existing `console_agent_sessions` table, run `ConsoleAgent.migrate!` to add the `name` column.
187
194
 
188
- ### OpenAI
195
+ ## Configuration
189
196
 
190
- Uses the Chat Completions API. Set `OPENAI_API_KEY` or `config.api_key`.
197
+ All settings live in `config/initializers/console_agent.rb` and can be changed at runtime:
191
198
 
192
199
  ```ruby
193
200
  ConsoleAgent.configure do |config|
194
- config.provider = :openai
195
- config.model = 'gpt-5.3-codex' # optional, this is the default
201
+ config.provider = :anthropic # or :openai
202
+ config.auto_execute = false # true to skip confirmations
203
+ config.max_tokens = 4096 # max tokens per LLM response
204
+ config.max_tool_rounds = 10 # max tool calls per query
205
+ config.session_logging = true # log sessions to DB (run ConsoleAgent.setup!)
196
206
  end
197
207
  ```
198
208
 
199
- ## Docker Setup
200
-
201
- If your Rails app runs in Docker, mount the gem source as a volume.
202
-
203
- In `docker-compose.yml`:
204
-
205
- ```yaml
206
- volumes:
207
- - /path/to/console_agent:/console_agent
208
- ```
209
-
210
- In `Gemfile`:
209
+ For the admin UI, mount the engine:
211
210
 
212
211
  ```ruby
213
- gem 'console_agent', path: '/console_agent', group: :development
212
+ mount ConsoleAgent::Engine => '/console_agent'
214
213
  ```
215
214
 
216
- For Docker builds (where the gem source isn't available yet), add a stub to your Dockerfile before `bundle install`:
217
-
218
- ```dockerfile
219
- RUN mkdir -p /console_agent/lib/console_agent && \
220
- echo "module ConsoleAgent; VERSION = '0.1.0'; end" > /console_agent/lib/console_agent/version.rb && \
221
- echo "require 'console_agent/version'" > /console_agent/lib/console_agent.rb && \
222
- printf "require_relative 'lib/console_agent/version'\nGem::Specification.new do |s|\n s.name = 'console_agent'\n s.version = ConsoleAgent::VERSION\n s.summary = 'stub'\n s.authors = ['x']\n s.files = ['lib/console_agent.rb']\n s.add_dependency 'rails', '>= 5.0'\n s.add_dependency 'faraday', '>= 1.0'\nend\n" > /console_agent/console_agent.gemspec
223
- ```
224
-
225
- The volume mount overwrites the stub at runtime with the real source.
226
-
227
215
  ## Requirements
228
216
 
229
- - Ruby >= 2.5
230
- - Rails >= 5.0
231
- - Faraday >= 1.0
217
+ - Ruby >= 2.5, Rails >= 5.0, Faraday >= 1.0
232
218
 
233
219
  ## License
234
220
 
@@ -0,0 +1,21 @@
1
+ module ConsoleAgent
2
+ class ApplicationController < ActionController::Base
3
+ protect_from_forgery with: :exception
4
+
5
+ before_action :authenticate!
6
+
7
+ private
8
+
9
+ def authenticate!
10
+ username = ConsoleAgent.configuration.admin_username
11
+ password = ConsoleAgent.configuration.admin_password
12
+
13
+ return unless username && password
14
+
15
+ authenticate_or_request_with_http_basic('ConsoleAgent Admin') do |u, p|
16
+ ActiveSupport::SecurityUtils.secure_compare(u, username) &
17
+ ActiveSupport::SecurityUtils.secure_compare(p, password)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,16 @@
1
+ module ConsoleAgent
2
+ class SessionsController < ApplicationController
3
+ PER_PAGE = 50
4
+
5
+ def index
6
+ @page = [params[:page].to_i, 1].max
7
+ @total = Session.count
8
+ @total_pages = (@total / PER_PAGE.to_f).ceil
9
+ @sessions = Session.recent.offset((@page - 1) * PER_PAGE).limit(PER_PAGE)
10
+ end
11
+
12
+ def show
13
+ @session = Session.find(params[:id])
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,42 @@
1
+ module ConsoleAgent
2
+ module SessionsHelper
3
+ # Convert ANSI escape codes to HTML spans for terminal-style rendering
4
+ def ansi_to_html(text)
5
+ return '' if text.nil? || text.empty?
6
+
7
+ color_map = {
8
+ '30' => '#000', '31' => '#e74c3c', '32' => '#2ecc71', '33' => '#f39c12',
9
+ '34' => '#3498db', '35' => '#9b59b6', '36' => '#1abc9c', '37' => '#ecf0f1',
10
+ '90' => '#888', '91' => '#ff6b6b', '92' => '#69db7c', '93' => '#ffd43b',
11
+ '94' => '#74c0fc', '95' => '#da77f2', '96' => '#63e6be', '97' => '#fff'
12
+ }
13
+
14
+ escaped = h(text).to_str
15
+
16
+ # Process ANSI codes: colors, bold, dim, reset
17
+ escaped.gsub!(/\e\[([0-9;]+)m/) do
18
+ codes = $1.split(';')
19
+ if codes.include?('0') || $1 == '0'
20
+ '</span>'
21
+ else
22
+ styles = []
23
+ codes.each do |code|
24
+ case code
25
+ when '1' then styles << 'font-weight:bold'
26
+ when '2' then styles << 'opacity:0.6'
27
+ when '4' then styles << 'text-decoration:underline'
28
+ else
29
+ styles << "color:#{color_map[code]}" if color_map[code]
30
+ end
31
+ end
32
+ styles.empty? ? '' : "<span style=\"#{styles.join(';')}\">"
33
+ end
34
+ end
35
+
36
+ # Clean up any remaining escape sequences
37
+ escaped.gsub!(/\e\[[0-9;]*[A-Za-z]/, '')
38
+
39
+ escaped.html_safe
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,23 @@
1
+ module ConsoleAgent
2
+ class Session < ActiveRecord::Base
3
+ self.table_name = 'console_agent_sessions'
4
+
5
+ validates :query, presence: true
6
+ validates :conversation, presence: true
7
+ validates :mode, presence: true, inclusion: { in: %w[one_shot interactive explain] }
8
+
9
+ scope :recent, -> { order(created_at: :desc) }
10
+ scope :named, ->(name) { where(name: name) }
11
+ scope :search, ->(q) { where("name LIKE ? OR query LIKE ?", "%#{q}%", "%#{q}%") }
12
+
13
+ def self.connection
14
+ klass = ConsoleAgent.configuration.connection_class
15
+ if klass
16
+ klass = Object.const_get(klass) if klass.is_a?(String)
17
+ klass.connection
18
+ else
19
+ super
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,57 @@
1
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
2
+ <h2 style="font-size: 20px;">Sessions</h2>
3
+ <span class="text-muted" style="font-size: 14px;"><%= @total %> total</span>
4
+ </div>
5
+
6
+ <% if @sessions.empty? %>
7
+ <div class="meta-card">
8
+ <p class="text-muted">No sessions recorded yet.</p>
9
+ </div>
10
+ <% else %>
11
+ <table>
12
+ <thead>
13
+ <tr>
14
+ <th>Time</th>
15
+ <th>User</th>
16
+ <th>Name</th>
17
+ <th>Query</th>
18
+ <th>Mode</th>
19
+ <th>Tokens</th>
20
+ <th>Executed?</th>
21
+ <th>Duration</th>
22
+ </tr>
23
+ </thead>
24
+ <tbody>
25
+ <% @sessions.each do |session| %>
26
+ <tr>
27
+ <td class="mono"><%= session.created_at.strftime('%Y-%m-%d %H:%M') %></td>
28
+ <td><%= session.user_name %></td>
29
+ <td><%= session.name.present? ? session.name : '-' %></td>
30
+ <td><a href="<%= console_agent.session_path(session) %>"><%= truncate(session.query, length: 80) %></a></td>
31
+ <td><span class="badge badge-<%= session.mode %>"><%= session.mode %></span></td>
32
+ <td class="mono"><%= session.input_tokens + session.output_tokens %></td>
33
+ <td><%= session.executed? ? '<span class="check">Yes</span>'.html_safe : '<span class="cross">No</span>'.html_safe %></td>
34
+ <td class="mono"><%= session.duration_ms ? "#{session.duration_ms}ms" : '-' %></td>
35
+ </tr>
36
+ <% end %>
37
+ </tbody>
38
+ </table>
39
+
40
+ <% if @total_pages > 1 %>
41
+ <div class="pagination">
42
+ <% if @page > 1 %>
43
+ <a href="<%= console_agent.sessions_path(page: @page - 1) %>">&larr; Newer</a>
44
+ <% else %>
45
+ <span class="disabled">&larr; Newer</span>
46
+ <% end %>
47
+
48
+ <span class="page-info">Page <%= @page %> of <%= @total_pages %></span>
49
+
50
+ <% if @page < @total_pages %>
51
+ <a href="<%= console_agent.sessions_path(page: @page + 1) %>">Older &rarr;</a>
52
+ <% else %>
53
+ <span class="disabled">Older &rarr;</span>
54
+ <% end %>
55
+ </div>
56
+ <% end %>
57
+ <% end %>
@@ -0,0 +1,56 @@
1
+ <a href="<%= console_agent.sessions_path %>" class="back-link">&larr; Back to sessions</a>
2
+
3
+ <div class="meta-card">
4
+ <div class="meta-grid">
5
+ <div class="meta-item">
6
+ <label>Query</label>
7
+ <span><%= @session.query %></span>
8
+ </div>
9
+ <% if @session.name.present? %>
10
+ <div class="meta-item">
11
+ <label>Name</label>
12
+ <span><%= @session.name %></span>
13
+ </div>
14
+ <% end %>
15
+ <div class="meta-item">
16
+ <label>Mode</label>
17
+ <span class="badge badge-<%= @session.mode %>"><%= @session.mode %></span>
18
+ </div>
19
+ <div class="meta-item">
20
+ <label>User</label>
21
+ <span><%= @session.user_name || '-' %></span>
22
+ </div>
23
+ <div class="meta-item">
24
+ <label>Provider / Model</label>
25
+ <span><%= @session.provider %> / <%= @session.model %></span>
26
+ </div>
27
+ <div class="meta-item">
28
+ <label>Tokens (in / out)</label>
29
+ <span class="mono"><%= @session.input_tokens %> / <%= @session.output_tokens %></span>
30
+ </div>
31
+ <div class="meta-item">
32
+ <label>Duration</label>
33
+ <span class="mono"><%= @session.duration_ms ? "#{@session.duration_ms}ms" : '-' %></span>
34
+ </div>
35
+ <div class="meta-item">
36
+ <label>Executed?</label>
37
+ <span><%= @session.executed? ? '<span class="check">Yes</span>'.html_safe : '<span class="cross">No</span>'.html_safe %></span>
38
+ </div>
39
+ <div class="meta-item">
40
+ <label>Time</label>
41
+ <span><%= @session.created_at.strftime('%Y-%m-%d %H:%M:%S') %></span>
42
+ </div>
43
+ </div>
44
+ </div>
45
+
46
+ <h3 style="font-size: 16px; margin-bottom: 12px;">Console Output</h3>
47
+
48
+ <% if @session.console_output.present? %>
49
+ <div class="terminal">
50
+ <pre><%= ansi_to_html(@session.console_output) %></pre>
51
+ </div>
52
+ <% else %>
53
+ <div class="meta-card">
54
+ <p class="text-muted">No console output recorded for this session.</p>
55
+ </div>
56
+ <% end %>