rails_console_ai 0.35.0 → 0.38.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 +4 -4
- data/CHANGELOG.md +25 -0
- data/README.md +102 -10
- data/app/controllers/rails_console_ai/artifacts_controller.rb +67 -0
- data/app/helpers/rails_console_ai/sessions_helper.rb +11 -11
- data/app/models/rails_console_ai/artifact.rb +231 -0
- data/app/views/layouts/rails_console_ai/application.html.erb +1 -0
- data/app/views/rails_console_ai/artifacts/index.html.erb +60 -0
- data/app/views/rails_console_ai/artifacts/show.html.erb +119 -0
- data/app/views/rails_console_ai/sessions/show.html.erb +6 -0
- data/config/routes.rb +8 -0
- data/lib/generators/rails_console_ai/templates/initializer.rb +12 -1
- data/lib/rails_console_ai/channel/console.rb +132 -18
- data/lib/rails_console_ai/channel/slack.rb +1 -1
- data/lib/rails_console_ai/configuration.rb +114 -15
- data/lib/rails_console_ai/context_builder.rb +48 -18
- data/lib/rails_console_ai/conversation_engine.rb +231 -61
- data/lib/rails_console_ai/line_editor.rb +142 -0
- data/lib/rails_console_ai/providers/anthropic.rb +3 -1
- data/lib/rails_console_ai/providers/base.rb +26 -1
- data/lib/rails_console_ai/providers/bedrock.rb +5 -2
- data/lib/rails_console_ai/providers/local.rb +17 -37
- data/lib/rails_console_ai/providers/openai.rb +31 -10
- data/lib/rails_console_ai/providers/openrouter.rb +108 -0
- data/lib/rails_console_ai/slack_bot.rb +58 -23
- data/lib/rails_console_ai/slash_commands.rb +150 -0
- data/lib/rails_console_ai/sub_agent.rb +35 -2
- data/lib/rails_console_ai/tools/registry.rb +233 -3
- data/lib/rails_console_ai/version.rb +1 -1
- data/lib/rails_console_ai.rb +223 -0
- metadata +8 -1
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
<div class="btn-bar">
|
|
2
|
+
<%= link_to '← All artifacts', artifacts_path, class: 'btn btn-secondary' %>
|
|
3
|
+
<%= link_to 'Download', download_artifact_path(@artifact.slug), class: 'btn' %>
|
|
4
|
+
<%= link_to 'Open raw', raw_artifact_path(@artifact.slug), class: 'btn btn-secondary', target: '_blank', rel: 'noopener noreferrer' %>
|
|
5
|
+
<%# Copies the artifact's source, exactly as stored. Fetches the raw URL on
|
|
6
|
+
click rather than embedding a second copy of the content in this page. %>
|
|
7
|
+
<button type="button" class="btn btn-secondary" id="copy-artifact"
|
|
8
|
+
data-url="<%= raw_artifact_path(@artifact.slug) %>"
|
|
9
|
+
title="Copy the artifact's content to the clipboard">Copy</button>
|
|
10
|
+
<%# button_to, not link_to: the engine layout loads no JavaScript, so a
|
|
11
|
+
data-method link would quietly do nothing. Matches skills/agents. %>
|
|
12
|
+
<span style="margin-left:auto;">
|
|
13
|
+
<%= button_to 'Delete', artifact_path(@artifact.slug), method: :delete,
|
|
14
|
+
class: 'btn btn-danger', form: { style: 'display:inline' },
|
|
15
|
+
data: { confirm: 'Delete this artifact? This cannot be undone.' } %>
|
|
16
|
+
</span>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<div class="meta-card">
|
|
20
|
+
<h2 style="font-size:18px; margin-bottom:4px;"><%= @artifact.name %></h2>
|
|
21
|
+
<% if @artifact.description.present? %>
|
|
22
|
+
<p class="text-muted" style="margin-bottom:16px;"><%= @artifact.description %></p>
|
|
23
|
+
<% end %>
|
|
24
|
+
<div class="meta-grid">
|
|
25
|
+
<div class="meta-item">
|
|
26
|
+
<label title="How the assistant addresses this artifact">Handle</label>
|
|
27
|
+
<span class="mono"><%= @artifact.slug %></span>
|
|
28
|
+
</div>
|
|
29
|
+
<div class="meta-item"><label>Revision</label><span><%= @artifact.revision %></span></div>
|
|
30
|
+
<div class="meta-item"><label>Type</label><span class="mono"><%= @artifact.content_type %></span></div>
|
|
31
|
+
<div class="meta-item"><label>Size</label><span><%= @artifact.human_size %></span></div>
|
|
32
|
+
<div class="meta-item"><label>Created by</label><span><%= @artifact.created_by || '—' %></span></div>
|
|
33
|
+
<div class="meta-item">
|
|
34
|
+
<label>Session</label>
|
|
35
|
+
<span>
|
|
36
|
+
<% if @artifact.session_id %>
|
|
37
|
+
<%= link_to "##{@artifact.session_id}", session_path(@artifact.session_id) %>
|
|
38
|
+
<% else %>
|
|
39
|
+
—
|
|
40
|
+
<% end %>
|
|
41
|
+
</span>
|
|
42
|
+
</div>
|
|
43
|
+
<div class="meta-item"><label>Created</label><span><%= @artifact.created_at.strftime('%Y-%m-%d %H:%M') %></span></div>
|
|
44
|
+
<div class="meta-item"><label>Updated</label><span><%= @artifact.updated_at.strftime('%Y-%m-%d %H:%M') %></span></div>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<% if @artifact.renderable? %>
|
|
49
|
+
<div class="meta-card" style="padding:0; overflow:hidden;">
|
|
50
|
+
<%#
|
|
51
|
+
Sandboxed with no allow-same-origin and no allow-scripts: the frame gets an
|
|
52
|
+
opaque origin, so even if the content carried a script it could not run, and
|
|
53
|
+
could not reach this page's session if it did. The same restrictions are set
|
|
54
|
+
as a CSP header on the raw response, which is what protects the "Open raw"
|
|
55
|
+
link where no iframe attribute applies.
|
|
56
|
+
%>
|
|
57
|
+
<iframe src="<%= raw_artifact_path(@artifact.slug) %>"
|
|
58
|
+
sandbox
|
|
59
|
+
referrerpolicy="no-referrer"
|
|
60
|
+
title="<%= @artifact.name %>"
|
|
61
|
+
style="width:100%; height:70vh; border:0; background:#fff; display:block;"></iframe>
|
|
62
|
+
</div>
|
|
63
|
+
<p class="text-muted" style="font-size:12px; margin-top:8px;">
|
|
64
|
+
Rendered in a sandboxed frame — scripts and network requests are blocked, so inline CSS shows but JavaScript will not run.
|
|
65
|
+
</p>
|
|
66
|
+
<% else %>
|
|
67
|
+
<div class="terminal">
|
|
68
|
+
<pre><%= @artifact.content %></pre>
|
|
69
|
+
</div>
|
|
70
|
+
<% end %>
|
|
71
|
+
|
|
72
|
+
<%# The only JavaScript in the admin UI. nonce: true picks up the host app's CSP
|
|
73
|
+
nonce when it has one, and is a no-op otherwise. %>
|
|
74
|
+
<%= javascript_tag nonce: true do %>
|
|
75
|
+
(function () {
|
|
76
|
+
var button = document.getElementById('copy-artifact');
|
|
77
|
+
if (!button) return;
|
|
78
|
+
var label = button.textContent;
|
|
79
|
+
|
|
80
|
+
function flash(text) {
|
|
81
|
+
button.textContent = text;
|
|
82
|
+
setTimeout(function () { button.textContent = label; }, 2000);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// navigator.clipboard needs a secure context (https or localhost). Over
|
|
86
|
+
// plain http elsewhere, fall back to the old textarea + execCommand route.
|
|
87
|
+
function writeClipboard(text) {
|
|
88
|
+
if (navigator.clipboard && window.isSecureContext) {
|
|
89
|
+
return navigator.clipboard.writeText(text);
|
|
90
|
+
}
|
|
91
|
+
return new Promise(function (resolve, reject) {
|
|
92
|
+
var area = document.createElement('textarea');
|
|
93
|
+
area.value = text;
|
|
94
|
+
area.setAttribute('readonly', '');
|
|
95
|
+
area.style.position = 'fixed';
|
|
96
|
+
area.style.top = '-1000px';
|
|
97
|
+
document.body.appendChild(area);
|
|
98
|
+
area.select();
|
|
99
|
+
var ok = false;
|
|
100
|
+
try { ok = document.execCommand('copy'); } catch (e) { ok = false; }
|
|
101
|
+
document.body.removeChild(area);
|
|
102
|
+
ok ? resolve() : reject(new Error('execCommand copy failed'));
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
button.addEventListener('click', function () {
|
|
107
|
+
button.disabled = true;
|
|
108
|
+
fetch(button.getAttribute('data-url'), { credentials: 'same-origin' })
|
|
109
|
+
.then(function (response) {
|
|
110
|
+
if (!response.ok) throw new Error('HTTP ' + response.status);
|
|
111
|
+
return response.text();
|
|
112
|
+
})
|
|
113
|
+
.then(writeClipboard)
|
|
114
|
+
.then(function () { flash('Copied'); })
|
|
115
|
+
.catch(function () { flash('Copy failed'); })
|
|
116
|
+
.then(function () { button.disabled = false; });
|
|
117
|
+
});
|
|
118
|
+
})();
|
|
119
|
+
<% end %>
|
|
@@ -36,6 +36,12 @@
|
|
|
36
36
|
<span><%= @session.slack_channel_name %></span>
|
|
37
37
|
</div>
|
|
38
38
|
<% end %>
|
|
39
|
+
<% if @session.try(:slack_thread_ts).present? %>
|
|
40
|
+
<div class="meta-item">
|
|
41
|
+
<label>Slack Thread</label>
|
|
42
|
+
<span class="mono"><%= @session.slack_thread_ts %></span>
|
|
43
|
+
</div>
|
|
44
|
+
<% end %>
|
|
39
45
|
<div class="meta-item">
|
|
40
46
|
<label>Provider / Model</label>
|
|
41
47
|
<span><%= @session.provider %> / <%= @session.model %></span>
|
data/config/routes.rb
CHANGED
|
@@ -2,6 +2,14 @@ RailsConsoleAi::Engine.routes.draw do
|
|
|
2
2
|
root to: 'sessions#index'
|
|
3
3
|
resources :sessions, only: [:index, :show]
|
|
4
4
|
|
|
5
|
+
# Addressed by token, not id — see ArtifactsController#load_artifact.
|
|
6
|
+
resources :artifacts, only: [:index, :show, :destroy] do
|
|
7
|
+
member do
|
|
8
|
+
get :raw
|
|
9
|
+
get :download
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
5
13
|
resources :skills do
|
|
6
14
|
member do
|
|
7
15
|
post :approve
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
RailsConsoleAi.configure do |config|
|
|
2
|
-
# LLM provider: :anthropic, :openai, or :local
|
|
2
|
+
# LLM provider: :anthropic, :openai, :openrouter, :bedrock, or :local
|
|
3
3
|
config.provider = :anthropic
|
|
4
4
|
|
|
5
5
|
# API key (or set ANTHROPIC_API_KEY / OPENAI_API_KEY env var)
|
|
@@ -32,6 +32,12 @@ RailsConsoleAi.configure do |config|
|
|
|
32
32
|
# config.local_model = 'qwen2.5:7b'
|
|
33
33
|
# config.local_api_key = nil
|
|
34
34
|
|
|
35
|
+
# OpenRouter provider (400+ models via unified API):
|
|
36
|
+
# config.provider = :openrouter
|
|
37
|
+
# config.model = 'anthropic/claude-sonnet-5'
|
|
38
|
+
# config.openrouter_site_url = 'https://yoursite.com' # optional, for attribution
|
|
39
|
+
# config.openrouter_app_name = 'MyApp' # optional, shown on openrouter.ai
|
|
40
|
+
|
|
35
41
|
# Slack: which users the bot responds to (legacy — prefer channels config below)
|
|
36
42
|
# config.slack_allowed_usernames = ['alice', 'bob']
|
|
37
43
|
|
|
@@ -47,6 +53,11 @@ RailsConsoleAi.configure do |config|
|
|
|
47
53
|
# Run RailsConsoleAi.setup! in the Rails console to create the table
|
|
48
54
|
config.session_logging = true
|
|
49
55
|
|
|
56
|
+
# Public base URL of this app, so links the assistant hands out (artifact URLs)
|
|
57
|
+
# are absolute — a bare path is useless once pasted into Slack or opened on a
|
|
58
|
+
# phone. Falls back to the app's default_url_options, then to a relative path.
|
|
59
|
+
# config.base_url = 'https://secure.example.com'
|
|
60
|
+
|
|
50
61
|
# Database connection for RailsConsoleAi tables (default: ActiveRecord::Base)
|
|
51
62
|
# Set to a class that responds to .connection if tables live on a different DB
|
|
52
63
|
# config.connection_class = Sharding::CentralizedModel
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
require 'readline'
|
|
2
1
|
require 'rails_console_ai/channel/base'
|
|
2
|
+
require 'rails_console_ai/line_editor'
|
|
3
|
+
require 'rails_console_ai/slash_commands'
|
|
3
4
|
|
|
4
5
|
module RailsConsoleAi
|
|
5
6
|
module Channel
|
|
@@ -186,6 +187,16 @@ module RailsConsoleAi
|
|
|
186
187
|
@interactive_console_capture = StringIO.new
|
|
187
188
|
@real_stdout = $stdout
|
|
188
189
|
$stdout = TeeIO.new(@real_stdout, @interactive_console_capture)
|
|
190
|
+
init_slash_commands
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def init_slash_commands
|
|
194
|
+
@slash_commands = SlashCommands.new
|
|
195
|
+
# Point the editor at the real stdout: Reline draws its completion menu
|
|
196
|
+
# through a Ruby IO, and $stdout is a TeeIO right now — without this the
|
|
197
|
+
# menu's escape codes get written into the saved session log.
|
|
198
|
+
@editor = LineEditor.resolve(RailsConsoleAi.configuration.line_editor, output: @real_stdout)
|
|
199
|
+
@editor.complete_with { @slash_commands.completion_entries }
|
|
189
200
|
end
|
|
190
201
|
|
|
191
202
|
def run_interactive_loop
|
|
@@ -196,14 +207,13 @@ module RailsConsoleAi
|
|
|
196
207
|
config = RailsConsoleAi.configuration
|
|
197
208
|
@real_stdout.puts "\e[2m Provider: #{config.provider} | Model: #{config.resolved_model}\e[0m"
|
|
198
209
|
safe_info = guards.empty? ? '' : " | Safe mode: #{guards.enabled? ? 'ON' : 'OFF'} (/danger to toggle)"
|
|
199
|
-
@real_stdout.puts "\e[2m Auto-execute: #{auto ? 'ON' : 'OFF'} (Shift-Tab or /auto to toggle)#{safe_info} | > code
|
|
210
|
+
@real_stdout.puts "\e[2m Auto-execute: #{auto ? 'ON' : 'OFF'} (Shift-Tab or /auto to toggle)#{safe_info} | > code\e[0m"
|
|
211
|
+
@real_stdout.puts "\e[2m Type / for commands, skills and agents#{@editor.is_a?(LineEditor::Reline_) ? ' (menu appears as you type)' : ' (Tab to complete)'}\e[0m"
|
|
200
212
|
|
|
201
|
-
|
|
202
|
-
Readline.parse_and_bind('"\e[Z": "\C-a\C-k/auto\C-m"')
|
|
203
|
-
end
|
|
213
|
+
@editor.bind_auto_toggle
|
|
204
214
|
|
|
205
215
|
loop do
|
|
206
|
-
input =
|
|
216
|
+
input = @editor.readline(@editor.prompt('ai> ', "\e[33m"))
|
|
207
217
|
break if input.nil?
|
|
208
218
|
|
|
209
219
|
input = input.strip
|
|
@@ -211,8 +221,13 @@ module RailsConsoleAi
|
|
|
211
221
|
break if input.downcase == 'exit' || input.downcase == 'quit'
|
|
212
222
|
next if input.empty?
|
|
213
223
|
|
|
214
|
-
|
|
215
|
-
|
|
224
|
+
# What the user typed, kept for history/logging even when a skill
|
|
225
|
+
# command expands into a much longer prompt.
|
|
226
|
+
typed = input
|
|
227
|
+
|
|
228
|
+
outcome = handle_slash_command(input)
|
|
229
|
+
next if outcome == :handled
|
|
230
|
+
input = outcome if outcome.is_a?(String)
|
|
216
231
|
|
|
217
232
|
# Direct code execution with ">" prefix
|
|
218
233
|
if input.start_with?('>') && !input.start_with?('>=')
|
|
@@ -220,14 +235,13 @@ module RailsConsoleAi
|
|
|
220
235
|
next
|
|
221
236
|
end
|
|
222
237
|
|
|
223
|
-
|
|
224
|
-
Readline::HISTORY.push(input) unless input == Readline::HISTORY.to_a.last
|
|
238
|
+
@editor.push_history(typed)
|
|
225
239
|
|
|
226
240
|
@engine.maybe_auto_upgrade_thinking(input)
|
|
227
241
|
|
|
228
|
-
@engine.set_interactive_query(
|
|
242
|
+
@engine.set_interactive_query(typed)
|
|
229
243
|
@engine.add_user_message(input)
|
|
230
|
-
@interactive_console_capture.write("ai> #{
|
|
244
|
+
@interactive_console_capture.write("ai> #{typed}\n")
|
|
231
245
|
@engine.log_interactive_turn
|
|
232
246
|
|
|
233
247
|
expected_stdout = $stdout
|
|
@@ -254,6 +268,9 @@ module RailsConsoleAi
|
|
|
254
268
|
|
|
255
269
|
@engine.log_interactive_turn
|
|
256
270
|
@engine.warn_if_history_large
|
|
271
|
+
# A turn may have created a skill or agent via save_skill/save_agent —
|
|
272
|
+
# drop the memo so the next "/" reflects it.
|
|
273
|
+
@slash_commands.refresh!
|
|
257
274
|
end
|
|
258
275
|
|
|
259
276
|
$stdout = @real_stdout
|
|
@@ -269,6 +286,9 @@ module RailsConsoleAi
|
|
|
269
286
|
$stderr.puts "\e[31mRailsConsoleAi Error: #{e.class}: #{e.message}\e[0m"
|
|
270
287
|
end
|
|
271
288
|
|
|
289
|
+
# Returns :handled when the command did its own thing, a String when it
|
|
290
|
+
# should be sent to the model as this turn's message, or nil when the input
|
|
291
|
+
# isn't a slash command at all.
|
|
272
292
|
def handle_slash_command(input)
|
|
273
293
|
case input
|
|
274
294
|
when '?', '/'
|
|
@@ -301,6 +321,8 @@ module RailsConsoleAi
|
|
|
301
321
|
@engine.upgrade_to_thinking_model
|
|
302
322
|
when '/unthink'
|
|
303
323
|
@engine.downgrade_from_thinking_model
|
|
324
|
+
when '/cheap'
|
|
325
|
+
@engine.switch_to_cheap_model
|
|
304
326
|
when /\A\/expand/
|
|
305
327
|
expand_id = input.sub('/expand', '').strip.to_i
|
|
306
328
|
full_output = expand_output(expand_id)
|
|
@@ -314,9 +336,47 @@ module RailsConsoleAi
|
|
|
314
336
|
when /\A\/name/
|
|
315
337
|
handle_name_command(input)
|
|
316
338
|
else
|
|
317
|
-
return
|
|
339
|
+
return dispatch_registry_command(input)
|
|
340
|
+
end
|
|
341
|
+
:handled
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
# Skills and agents are addressed by a slug derived from their name:
|
|
345
|
+
# "Restart user trial" -> /restart-user-trial.
|
|
346
|
+
def dispatch_registry_command(input)
|
|
347
|
+
return nil unless input.start_with?('/')
|
|
348
|
+
|
|
349
|
+
slug, _, args = input[1..].partition(/\s+/)
|
|
350
|
+
command = @slash_commands.find(slug)
|
|
351
|
+
|
|
352
|
+
unless command
|
|
353
|
+
# Only claim the input if it actually looks like a command. A message
|
|
354
|
+
# that merely opens with a path ("/tmp/foo.log has the error") must
|
|
355
|
+
# still reach the model untouched.
|
|
356
|
+
return nil unless slug =~ /\A[a-z0-9][a-z0-9_-]*\z/
|
|
357
|
+
warn_unknown_command(slug)
|
|
358
|
+
return :handled
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
case command.kind
|
|
362
|
+
when :skill
|
|
363
|
+
@engine.skill_command_prompt(command.record, args)
|
|
364
|
+
when :agent
|
|
365
|
+
@engine.run_agent_command(command.record, args)
|
|
366
|
+
:handled
|
|
367
|
+
else
|
|
368
|
+
nil # a built-in, already handled by the case above
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
def warn_unknown_command(slug)
|
|
373
|
+
@real_stdout.puts "\e[33m Unknown command: /#{slug}\e[0m"
|
|
374
|
+
near = @slash_commands.candidates.select { |c| c.start_with?("/#{slug[0, 3]}") }
|
|
375
|
+
if near.empty?
|
|
376
|
+
@real_stdout.puts "\e[2m Type / to see everything available.\e[0m"
|
|
377
|
+
else
|
|
378
|
+
@real_stdout.puts "\e[2m Did you mean: #{near.first(5).join(', ')}?\e[0m"
|
|
318
379
|
end
|
|
319
|
-
true
|
|
320
380
|
end
|
|
321
381
|
|
|
322
382
|
def retry_last_code
|
|
@@ -325,7 +385,7 @@ module RailsConsoleAi
|
|
|
325
385
|
|
|
326
386
|
def handle_direct_execution(input)
|
|
327
387
|
raw_code = input.sub(/\A>\s?/, '')
|
|
328
|
-
|
|
388
|
+
@editor.push_history(input)
|
|
329
389
|
@interactive_console_capture.write("ai> #{input}\n")
|
|
330
390
|
@engine.execute_direct(raw_code)
|
|
331
391
|
@engine.log_interactive_turn
|
|
@@ -367,13 +427,14 @@ module RailsConsoleAi
|
|
|
367
427
|
def display_model_info
|
|
368
428
|
config = RailsConsoleAi.configuration
|
|
369
429
|
model = @engine.effective_model
|
|
370
|
-
thinking = config.resolved_thinking_model
|
|
371
430
|
pricing = Configuration.pricing_for(model)
|
|
372
431
|
|
|
373
432
|
@real_stdout.puts "\e[36m Model info:\e[0m"
|
|
374
433
|
@real_stdout.puts "\e[2m Provider: #{config.provider}\e[0m"
|
|
375
|
-
@real_stdout.puts "\e[2m Model: #{
|
|
376
|
-
@real_stdout.puts "\e[2m
|
|
434
|
+
@real_stdout.puts "\e[2m Model: #{@engine.effective_model_label} [#{@engine.model_tier}]\e[0m"
|
|
435
|
+
@real_stdout.puts "\e[2m Default model: #{model_with_effort(config.resolved_model, config.effort)}\e[0m"
|
|
436
|
+
@real_stdout.puts "\e[2m Thinking model: #{model_with_effort(config.resolved_thinking_model, config.thinking_effort)}\e[0m"
|
|
437
|
+
@real_stdout.puts "\e[2m Cheap model: #{config.resolved_cheap_model}\e[0m"
|
|
377
438
|
@real_stdout.puts "\e[2m Max tokens: #{config.resolved_max_tokens}\e[0m"
|
|
378
439
|
if pricing
|
|
379
440
|
@real_stdout.puts "\e[2m Pricing: $#{pricing[:input] * 1_000_000}/M in, $#{pricing[:output] * 1_000_000}/M out\e[0m"
|
|
@@ -383,6 +444,12 @@ module RailsConsoleAi
|
|
|
383
444
|
end
|
|
384
445
|
@real_stdout.puts "\e[2m Bedrock region: #{config.bedrock_region}\e[0m" if config.provider == :bedrock
|
|
385
446
|
@real_stdout.puts "\e[2m Local URL: #{config.local_url}\e[0m" if config.provider == :local
|
|
447
|
+
@real_stdout.puts "\e[2m OpenRouter URL: #{config.openrouter_url}\e[0m" if config.openrouter_url
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
def model_with_effort(model, effort)
|
|
451
|
+
effort = Configuration.effort_for(model, effort)
|
|
452
|
+
effort ? "#{model} (#{effort} effort)" : model
|
|
386
453
|
end
|
|
387
454
|
|
|
388
455
|
def handle_name_command(input)
|
|
@@ -412,6 +479,7 @@ module RailsConsoleAi
|
|
|
412
479
|
@real_stdout.puts "\e[2m /model Show provider, model, and pricing info\e[0m"
|
|
413
480
|
@real_stdout.puts "\e[2m /think Switch to thinking model\e[0m"
|
|
414
481
|
@real_stdout.puts "\e[2m /unthink Switch back to default model\e[0m"
|
|
482
|
+
@real_stdout.puts "\e[2m /cheap Switch to the cheaper, faster model\e[0m"
|
|
415
483
|
@real_stdout.puts "\e[2m /compact Summarize conversation to reduce context\e[0m"
|
|
416
484
|
@real_stdout.puts "\e[2m /usage Show session token totals\e[0m"
|
|
417
485
|
@real_stdout.puts "\e[2m /cost Show cost estimate by model\e[0m"
|
|
@@ -424,6 +492,52 @@ module RailsConsoleAi
|
|
|
424
492
|
@real_stdout.puts "\e[2m > code Execute Ruby directly (skip LLM)\e[0m"
|
|
425
493
|
@real_stdout.puts "\e[2m Ctrl-C Cancel the current operation\e[0m"
|
|
426
494
|
@real_stdout.puts "\e[2m exit/quit Leave interactive mode\e[0m"
|
|
495
|
+
display_registry_help
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
# Skills and agents, listed the same way as the built-ins so "/" answers
|
|
499
|
+
# "what can I run?" in one place.
|
|
500
|
+
def display_registry_help
|
|
501
|
+
@slash_commands.refresh!
|
|
502
|
+
|
|
503
|
+
skills = @slash_commands.skills
|
|
504
|
+
agents = @slash_commands.agents
|
|
505
|
+
# Size the column to what's actually listed, and don't let one very long
|
|
506
|
+
# name push every description off the right edge.
|
|
507
|
+
width = [(skills + agents).map { |c| c.slug.length }.max.to_i + 2, 34].min
|
|
508
|
+
|
|
509
|
+
unless skills.empty?
|
|
510
|
+
@real_stdout.puts
|
|
511
|
+
@real_stdout.puts "\e[36m Skills:\e[0m \e[2m(runs here, following the recipe)\e[0m"
|
|
512
|
+
skills.each { |c| @real_stdout.puts registry_help_row(c, width) }
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
unless agents.empty?
|
|
516
|
+
@real_stdout.puts
|
|
517
|
+
@real_stdout.puts "\e[36m Agents:\e[0m \e[2m(runs in a separate context, reports back)\e[0m"
|
|
518
|
+
agents.each { |c| @real_stdout.puts registry_help_row(c, width) }
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
return if skills.empty? && agents.empty?
|
|
522
|
+
@real_stdout.puts
|
|
523
|
+
@real_stdout.puts "\e[2m Pass a request after the name: /#{(skills.first || agents.first).slug} <what you want>\e[0m"
|
|
524
|
+
end
|
|
525
|
+
|
|
526
|
+
# One "/slug description" line, clipped so a long description wraps into
|
|
527
|
+
# the next row instead of making the list unreadable.
|
|
528
|
+
def registry_help_row(command, width)
|
|
529
|
+
room = terminal_width - width - 6
|
|
530
|
+
desc = command.description.to_s
|
|
531
|
+
desc = "#{desc[0, room - 1]}\u2026" if room > 10 && desc.length > room
|
|
532
|
+
"\e[2m /#{command.slug.ljust(width)} #{desc}\e[0m"
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
def terminal_width
|
|
536
|
+
require 'io/console'
|
|
537
|
+
cols = IO.console && IO.console.winsize.last.to_i
|
|
538
|
+
cols && cols > 40 ? cols : 100
|
|
539
|
+
rescue StandardError
|
|
540
|
+
100
|
|
427
541
|
end
|
|
428
542
|
|
|
429
543
|
def display_exit_info
|
|
@@ -71,7 +71,7 @@ module RailsConsoleAi
|
|
|
71
71
|
stripped = strip_ansi(text).strip
|
|
72
72
|
return if stripped.empty?
|
|
73
73
|
|
|
74
|
-
if stripped =~ /\AThinking\.\.\.|\AAttempting to fix|\ACancelled|\
|
|
74
|
+
if stripped =~ /\AThinking\.\.\.|\AAttempting to fix|\ACancelled|\A_Session\b/
|
|
75
75
|
post(stripped)
|
|
76
76
|
else
|
|
77
77
|
@output_log.write("#{stripped}\n")
|