console_agent 0.2.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: 53925e32cd4919e550bfdc081ede866613ce3ea18a585aca6cb81d1687ce3901
4
- data.tar.gz: ae601be1b81ff2fcb89addda1c008c0646118f2af21c9009a1792556ffd9b4ed
3
+ metadata.gz: 661ab1a997f36d2b10ea9649aa41367aca039c859c3e2afa01c3cbf8b8dd9345
4
+ data.tar.gz: 5cfe513d78a1785b7199fb0ebbbc70e825092e49e4051caf390e3ccb7ccf23c4
5
5
  SHA512:
6
- metadata.gz: 54ec67e116e4a05de7a5d915f93dc7babf2d64d2b008bc507718dce92d58ad032beeaa1d7710d8190943454f937f91780ff5614e44f5ef173ab5feff6dddb339
7
- data.tar.gz: f12af224368e44b149d740f8d0b2dee4a38ce86564ee9337c5a8ed569a83bb387d3edcf2ae76142d4b510f507c5e6d64d82aab70c16eb94f66d83e3811eb0cd6
6
+ metadata.gz: 0d7fd63a81886c7abbf4f82db677b6b4bb9151760c378901306e789b3619d7ca31b743f7f4a14a9a28335e775877243b0c4ff8b8bd6cc6acee0c8a02c270eccc
7
+ data.tar.gz: 3e0308bd89e0421a4a29dbe8c7b7be6afd584a504559a456f830e1c9a268517d131d6dcf00417347f5f3ea80a434bf9211e0515771fc3e43686f0117bf401f9b
data/README.md CHANGED
@@ -1,60 +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
- It's like Claude Code embedded in your Rails Console.
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
- 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.
8
-
9
- ## Quick Start
10
-
11
- Add to your Gemfile:
7
+ ## Install
12
8
 
13
9
  ```ruby
10
+ # Gemfile
14
11
  gem 'console_agent', group: :development
15
12
  ```
16
13
 
17
- Run the install generator:
18
-
19
14
  ```bash
20
15
  bundle install
21
16
  rails generate console_agent:install
22
17
  ```
23
18
 
24
- Set your API key (pick one):
25
-
26
- ```bash
27
- # Option A: environment variable
28
- export ANTHROPIC_API_KEY=sk-ant-...
29
-
30
- # Option B: in the initializer
31
- # config.api_key = 'sk-ant-...'
32
- ```
33
-
34
- Open a console and go:
19
+ Set your API key in the generated initializer or as an env var (`ANTHROPIC_API_KEY`):
35
20
 
36
21
  ```ruby
37
- rails console
38
- ai "show me all users who signed up this week"
22
+ # config/initializers/console_agent.rb
23
+ ConsoleAgent.configure do |config|
24
+ config.api_key = 'sk-ant-...'
25
+ end
39
26
  ```
40
27
 
41
- You can also set or change your API key at runtime in the console:
28
+ To set up session logging (OPTIONAL), create the table from the console:
42
29
 
43
30
  ```ruby
44
- ConsoleAgent.configure { |c| c.api_key = 'sk-ant-...' }
31
+ ConsoleAgent.setup!
32
+ # => ConsoleAgent: created console_agent_sessions table.
45
33
  ```
46
34
 
47
- ## Console Commands
35
+ To reset the table (e.g. after upgrading), run `ConsoleAgent.teardown!` then `ConsoleAgent.setup!`.
48
36
 
49
- | Command | Description |
50
- |---------|-------------|
51
- | `ai "query"` | Ask a question, review generated code, confirm before executing |
52
- | `ai! "query"` | Ask a question and enter interactive mode for follow-ups |
53
- | `ai!` | Enter interactive mode (type `exit` to leave) |
54
- | `ai? "query"` | Explain only — shows code but never executes |
55
- | `ai_status` | Print current configuration summary |
56
-
57
- ### Example Session
37
+ ## Usage
58
38
 
59
39
  ```
60
40
  irb> ai "find the 5 most recent orders over $100"
@@ -63,272 +43,178 @@ irb> ai "find the 5 most recent orders over $100"
63
43
  12 tables: users, orders, line_items, products...
64
44
  -> describe_table("orders")
65
45
  8 columns
66
- -> describe_model("Order")
67
- 4 associations, 2 validations
68
-
69
- You can query recent high-value orders like this:
70
46
 
71
47
  Order.where("total > ?", 100).order(created_at: :desc).limit(5)
72
48
 
73
- [tokens in: 1,240 | out: 85 | total: 1,325]
74
49
  Execute? [y/N/edit] y
75
50
  => [#<Order id: 4821, ...>, ...]
76
51
  ```
77
52
 
78
- ### 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.
79
54
 
80
- ```
81
- irb> ai!
82
- ConsoleAgent interactive mode. Type 'exit' or 'quit' to leave.
83
- ai> show me all tables
84
- Thinking...
85
- -> list_tables
86
- 12 tables: users, orders, line_items, products...
87
- ...
88
- ai> now count orders by status
89
- ...
90
- ai> exit
91
- [session totals — in: 3,200 | out: 410 | total: 3,610]
92
- Left ConsoleAgent interactive mode.
93
- ```
55
+ ### Commands
94
56
 
95
- ### Configuration Status
96
-
97
- ```
98
- irb> ai_status
99
- [ConsoleAgent v0.1.0]
100
- Provider: anthropic
101
- Model: claude-opus-4-6
102
- API key: sk-ant-...a3b4
103
- Context mode: smart
104
- Max tokens: 4096
105
- Temperature: 0.2
106
- Timeout: 30s
107
- Max tool rounds:10
108
- Auto-execute: false
109
- Debug: false
110
- ```
111
-
112
- ## Configuration
113
-
114
- The install generator creates `config/initializers/console_agent.rb`:
115
-
116
- ```ruby
117
- ConsoleAgent.configure do |config|
118
- # LLM provider: :anthropic or :openai
119
- config.provider = :anthropic
120
-
121
- # API key (or set ANTHROPIC_API_KEY / OPENAI_API_KEY env var)
122
- # config.api_key = 'sk-...'
123
-
124
- # Model override (defaults: claude-opus-4-6 for Anthropic, gpt-5.3-codex for OpenAI)
125
- # config.model = 'claude-opus-4-6'
126
-
127
- # Context mode:
128
- # :smart - (default) LLM uses tools to fetch schema/model/code on demand
129
- # :full - sends all schema, models, and routes every time
130
- config.context_mode = :smart
131
-
132
- # Max tokens for LLM response
133
- config.max_tokens = 4096
134
-
135
- # Temperature (0.0 - 1.0)
136
- config.temperature = 0.2
137
-
138
- # Auto-execute generated code without confirmation
139
- config.auto_execute = false
140
-
141
- # Max tool-use rounds per query in :smart mode
142
- config.max_tool_rounds = 10
143
-
144
- # HTTP timeout in seconds
145
- config.timeout = 30
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 |
146
62
 
147
- # Debug mode: prints full API requests/responses and tool calls
148
- # config.debug = true
149
- end
150
- ```
63
+ ### Multi-Step Plans
151
64
 
152
- All settings can be changed at runtime in the console:
65
+ For complex tasks, the AI builds a plan and executes it step by step:
153
66
 
154
- ```ruby
155
- ConsoleAgent.configure { |c| c.api_key = 'sk-ant-...' }
156
- ConsoleAgent.configure { |c| c.debug = true }
157
- ConsoleAgent.configure { |c| c.provider = :openai; c.api_key = 'sk-...' }
158
67
  ```
68
+ ai> get the most recent salesforce token and count events via the API
69
+ Thinking...
70
+ -> describe_table("oauth2_tokens")
71
+ 28 columns
72
+ -> read_file("lib/salesforce_api.rb")
73
+ 202 lines
159
74
 
160
- ## Context Modes
161
-
162
- ### Smart Mode (default)
163
-
164
- The LLM gets a minimal system prompt and uses tools to look up what it needs:
165
-
166
- - **list_tables** / **describe_table** — database schema on demand
167
- - **list_models** / **describe_model** — ActiveRecord associations, validations
168
- - **list_files** / **read_file** / **search_code** — browse app source code
169
-
170
- This keeps token usage low (~1-3K per query) even for large apps with hundreds of tables.
171
-
172
- ### Full Mode
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")
173
82
 
174
- 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.
83
+ Accept plan? [y/N/a(uto)] a
84
+ Step 1/2: Find the most recent active Salesforce OAuth2 token
85
+ ...
86
+ => #<Oauth2Token id: 1, provider: "salesforce", ...>
175
87
 
176
- ```ruby
177
- ConsoleAgent.configure { |c| c.context_mode = :full }
88
+ Step 2/2: Query event count via SOQL
89
+ ...
90
+ => [{"expr0"=>42}]
178
91
  ```
179
92
 
180
- ## Tools Available in Smart Mode
181
-
182
- | Tool | Description |
183
- |------|-------------|
184
- | `list_tables` | All database table names |
185
- | `describe_table` | Columns, types, and indexes for one table |
186
- | `list_models` | All model names with association names |
187
- | `describe_model` | Associations, validations, scopes for one model |
188
- | `list_files` | Ruby files in a directory |
189
- | `read_file` | Read a source file (capped at 200 lines) |
190
- | `search_code` | Grep for a pattern across Ruby files |
191
- | `ask_user` | Ask the console user a clarifying question |
192
- | `save_memory` | Persist a learned fact for future sessions |
193
- | `recall_memories` | Search saved memories |
194
- | `load_skill` | Load full instructions for a skill |
195
-
196
- The LLM decides which tools to call based on your question. You can see the tool calls happening in real time.
93
+ Each step's return value is available to later steps as `step1`, `step2`, etc.
197
94
 
198
- ## Memories & Skills
199
-
200
- ConsoleAgent can remember what it learns about your codebase across sessions and load reusable skill instructions on demand.
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
201
99
 
202
100
  ### Memories
203
101
 
204
- When the AI investigates something complex (like how sharding works in your app), it can save what it learned for next time. Memories are stored in `.console_agent/memories.yml` in your Rails app.
102
+ The AI remembers what it learns about your codebase across sessions:
205
103
 
206
104
  ```
207
- ai> how many users are on this shard?
208
- Thinking...
209
- -> search_code("shard")
210
- Found 50 matches
105
+ ai> how does sharding work?
211
106
  -> read_file("config/initializers/sharding.rb")
212
- 202 lines
213
107
  -> save_memory("Sharding architecture")
214
- Memory saved: "Sharding architecture"
108
+ Memory saved
215
109
 
216
- Each shard is a separate database. User.count returns the count for the current shard only.
217
-
218
- User.count
219
-
220
- [tokens in: 2,340 | out: 120 | total: 2,460]
110
+ This app uses database-per-shard. User.count returns the current shard only.
221
111
  ```
222
112
 
223
- Next session, the AI already knows:
224
-
225
- ```
226
- ai> count users on this shard
227
- Thinking...
113
+ Next time, it already knows — no re-reading files, fewer tokens.
228
114
 
229
- User.count
115
+ ### Interactive Mode
230
116
 
231
- [tokens in: 580 | out: 45 | total: 625]
232
117
  ```
118
+ irb> ai!
119
+ ConsoleAgent interactive mode. Type 'exit' to leave.
120
+ Auto-execute: OFF (Shift-Tab or /auto to toggle)
233
121
 
234
- The memory file is human-editable YAML:
235
-
236
- ```yaml
237
- # .console_agent/memories.yml
238
- memories:
239
- - id: mem_1709123456_42
240
- name: Sharding architecture
241
- description: "App uses database-per-shard. Each shard is a separate DB. User.count only counts the current shard."
242
- tags: [database, sharding]
243
- created_at: "2026-02-27T10:30:00Z"
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
244
131
  ```
245
132
 
246
- You can commit this file to your repo so the whole team benefits, or gitignore it for personal use.
133
+ Toggle `/auto` to skip confirmation prompts. `/debug` shows raw API traffic. `/usage` shows token stats.
247
134
 
248
- ### Skills
135
+ ### Sessions
249
136
 
250
- Skills are reusable instruction files that teach the AI how to handle specific tasks. Store them in `.console_agent/skills/` as markdown files with YAML frontmatter:
137
+ Sessions are saved automatically when session logging is enabled. You can name, list, and resume them.
251
138
 
252
- ```markdown
253
- # .console_agent/skills/sharding.md
254
- ---
255
- name: Sharded database queries
256
- description: How to query across database shards. Use when user asks about counting records across shards or shard-specific operations.
257
- ---
139
+ ```
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
+ ```
258
147
 
259
- ## Instructions
148
+ List recent sessions:
260
149
 
261
- This app uses Apartment with one database per shard.
262
- - `User.count` only counts the current shard
263
- - To count across all shards: `Shard.all.sum { |s| s.switch { User.count } }`
264
- - Current shard: `Apartment::Tenant.current`
265
150
  ```
151
+ irb> ai_sessions
152
+ [Sessions — showing 3]
266
153
 
267
- Only the skill name and description are sent to the AI on every request (~100 tokens per skill). The full instructions are loaded on demand via the `load_skill` tool only when relevant.
268
-
269
- ### Storage
154
+ #42 sf_user_123_calendar find user 123 with calendar issues
155
+ [interactive] 5m ago 2340 tokens
270
156
 
271
- By default, memories and skills are stored on the filesystem at `Rails.root/.console_agent/`. This works for development and for production environments with a persistent filesystem.
157
+ #41 count all active users
158
+ [one_shot] 1h ago 850 tokens
272
159
 
273
- For containers or other environments where the filesystem is ephemeral, you have two options:
160
+ #40 debug_payments explain payment flow
161
+ [interactive] 2h ago 4100 tokens
274
162
 
275
- **Option A: Commit to your repo.** Create `.console_agent/memories.yml` and `.console_agent/skills/*.md` in your codebase. They'll be baked into your Docker image.
163
+ Use ai_resume(id_or_name) to resume a session.
164
+ ```
276
165
 
277
- **Option B: Custom storage adapter.** Implement the storage interface and plug it in:
166
+ Resume a session by name or ID previous output is replayed, then you continue where you left off:
278
167
 
279
- ```ruby
280
- ConsoleAgent.configure do |config|
281
- config.storage_adapter = MyS3Storage.new(bucket: 'my-bucket', prefix: 'console_agent/')
282
- end
283
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 ---
284
174
 
285
- A storage adapter just needs four methods: `read(key)`, `write(key, content)`, `list(pattern)`, and `exists?(key)`.
175
+ ConsoleAgent interactive mode (sf_user_123_calendar). Type 'exit' to leave.
176
+ ai> now check their calendar sync status
177
+ ...
178
+ ```
286
179
 
287
- To disable memories or skills:
180
+ Name or rename a session after the fact:
288
181
 
289
- ```ruby
290
- ConsoleAgent.configure do |config|
291
- config.memories_enabled = false
292
- config.skills_enabled = false
293
- end
182
+ ```
183
+ irb> ai_name 41, "active_user_count"
184
+ Session #41 named: active_user_count
294
185
  ```
295
186
 
296
- ## Providers
187
+ Filter sessions by search term:
297
188
 
298
- ### Anthropic (default)
189
+ ```
190
+ irb> ai_sessions 20, search: "salesforce"
191
+ ```
299
192
 
300
- 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.
301
194
 
302
- ### OpenAI
195
+ ## Configuration
303
196
 
304
- 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:
305
198
 
306
199
  ```ruby
307
200
  ConsoleAgent.configure do |config|
308
- config.provider = :openai
309
- 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!)
310
206
  end
311
207
  ```
312
208
 
313
- ## Local Development
314
-
315
- To develop the gem locally against a Rails app, use a path reference in your Gemfile:
316
-
317
- ```ruby
318
- gem 'console_agent', path: '/path/to/console_agent'
319
- ```
320
-
321
- Switch back to the published gem when you're done:
209
+ For the admin UI, mount the engine:
322
210
 
323
211
  ```ruby
324
- gem 'console_agent'
212
+ mount ConsoleAgent::Engine => '/console_agent'
325
213
  ```
326
214
 
327
215
  ## Requirements
328
216
 
329
- - Ruby >= 2.5
330
- - Rails >= 5.0
331
- - Faraday >= 1.0
217
+ - Ruby >= 2.5, Rails >= 5.0, Faraday >= 1.0
332
218
 
333
219
  ## License
334
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 %>