aia 0.9.12 → 0.9.13
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/.envrc +2 -0
- data/.version +1 -1
- data/lib/aia/chat_processor_service.rb +19 -4
- data/lib/aia/config/cli_parser.rb +9 -0
- data/lib/aia/ruby_llm_adapter.rb +66 -8
- data/lib/aia/session.rb +27 -4
- data/lib/aia/ui_presenter.rb +206 -0
- metadata +3 -18
- data/_notes.txt +0 -231
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d5d9ca1e0f36d50857345bcad1268359e8435c16ba2c159d68482c070de59207
|
4
|
+
data.tar.gz: 20e10d41131b850e7320613f0959d8c94589e48422bc5ba948954c614c094c12
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bb7913215b44a686db90c9e9e4a69a00024611c0a28f7f06da4290ad7781ef9685288845ea17f3c4180f74badf4d0829a462596a0280aed91354b6d54a36f975
|
7
|
+
data.tar.gz: 1701b67da9e0aa8afa4cd1815422989544c191e50a38f58205984b1d96033d4a6dea27fe1b9efceb9307b231848fd9cc6440bd83203a9b03dfa7c5dbee700fd2
|
data/.envrc
CHANGED
data/.version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.9.
|
1
|
+
0.9.13
|
@@ -28,11 +28,26 @@ module AIA
|
|
28
28
|
result = send_to_client(prompt)
|
29
29
|
end
|
30
30
|
|
31
|
-
|
32
|
-
|
31
|
+
# Preserve token information if available for metrics
|
32
|
+
if result.is_a?(String)
|
33
|
+
{ content: result, metrics: nil }
|
34
|
+
elsif result.respond_to?(:multi_model?) && result.multi_model?
|
35
|
+
# Handle multi-model response with metrics
|
36
|
+
{
|
37
|
+
content: result.content,
|
38
|
+
metrics: nil, # Individual model metrics handled separately
|
39
|
+
multi_metrics: result.metrics_list
|
40
|
+
}
|
41
|
+
else
|
42
|
+
{
|
43
|
+
content: result.content,
|
44
|
+
metrics: {
|
45
|
+
input_tokens: result.input_tokens,
|
46
|
+
output_tokens: result.output_tokens,
|
47
|
+
model_id: result.model_id
|
48
|
+
}
|
49
|
+
}
|
33
50
|
end
|
34
|
-
|
35
|
-
result
|
36
51
|
end
|
37
52
|
|
38
53
|
|
@@ -250,6 +250,15 @@ module AIA
|
|
250
250
|
config.completion = shell
|
251
251
|
end
|
252
252
|
|
253
|
+
opts.on("--metrics", "Display token usage in chat mode") do
|
254
|
+
config.show_metrics = true
|
255
|
+
end
|
256
|
+
|
257
|
+
opts.on("--cost", "Include cost calculations with metrics (requires --metrics)") do
|
258
|
+
config.show_cost = true
|
259
|
+
config.show_metrics = true # Automatically enable metrics when cost is requested
|
260
|
+
end
|
261
|
+
|
253
262
|
opts.on("--version", "Show version") do
|
254
263
|
puts AIA::VERSION
|
255
264
|
exit
|
data/lib/aia/ruby_llm_adapter.rb
CHANGED
@@ -310,9 +310,15 @@ module AIA
|
|
310
310
|
prompt_parts << ""
|
311
311
|
|
312
312
|
results.each do |model_name, result|
|
313
|
-
|
313
|
+
# Extract content from RubyLLM::Message if needed
|
314
|
+
content = if result.respond_to?(:content)
|
315
|
+
result.content
|
316
|
+
else
|
317
|
+
result.to_s
|
318
|
+
end
|
319
|
+
next if content.start_with?("Error with")
|
314
320
|
prompt_parts << "#{model_name}:"
|
315
|
-
prompt_parts <<
|
321
|
+
prompt_parts << content
|
316
322
|
prompt_parts << ""
|
317
323
|
end
|
318
324
|
|
@@ -321,13 +327,64 @@ module AIA
|
|
321
327
|
end
|
322
328
|
|
323
329
|
def format_individual_responses(results)
|
324
|
-
|
330
|
+
# For metrics support, return a special structure if all results have token info
|
331
|
+
has_metrics = results.values.all? { |r| r.respond_to?(:input_tokens) && r.respond_to?(:output_tokens) }
|
332
|
+
|
333
|
+
if has_metrics && AIA.config.show_metrics
|
334
|
+
# Return structured data that preserves metrics for multi-model
|
335
|
+
format_multi_model_with_metrics(results)
|
336
|
+
else
|
337
|
+
# Original string formatting for non-metrics mode
|
338
|
+
output = []
|
339
|
+
results.each do |model_name, result|
|
340
|
+
output << "from: #{model_name}"
|
341
|
+
# Extract content from RubyLLM::Message if needed
|
342
|
+
content = if result.respond_to?(:content)
|
343
|
+
result.content
|
344
|
+
else
|
345
|
+
result.to_s
|
346
|
+
end
|
347
|
+
output << content
|
348
|
+
output << "" # Add blank line between results
|
349
|
+
end
|
350
|
+
output.join("\n")
|
351
|
+
end
|
352
|
+
end
|
353
|
+
|
354
|
+
def format_multi_model_with_metrics(results)
|
355
|
+
# Create a composite response that includes all model responses and metrics
|
356
|
+
formatted_content = []
|
357
|
+
metrics_data = []
|
358
|
+
|
325
359
|
results.each do |model_name, result|
|
326
|
-
|
327
|
-
|
328
|
-
|
360
|
+
formatted_content << "from: #{model_name}"
|
361
|
+
formatted_content << result.content
|
362
|
+
formatted_content << ""
|
363
|
+
|
364
|
+
# Collect metrics for each model
|
365
|
+
metrics_data << {
|
366
|
+
model_id: model_name,
|
367
|
+
input_tokens: result.input_tokens,
|
368
|
+
output_tokens: result.output_tokens
|
369
|
+
}
|
370
|
+
end
|
371
|
+
|
372
|
+
# Return a special MultiModelResponse that ChatProcessorService can handle
|
373
|
+
MultiModelResponse.new(formatted_content.join("\n"), metrics_data)
|
374
|
+
end
|
375
|
+
|
376
|
+
# Helper class to carry multi-model response with metrics
|
377
|
+
class MultiModelResponse
|
378
|
+
attr_reader :content, :metrics_list
|
379
|
+
|
380
|
+
def initialize(content, metrics_list)
|
381
|
+
@content = content
|
382
|
+
@metrics_list = metrics_list
|
383
|
+
end
|
384
|
+
|
385
|
+
def multi_model?
|
386
|
+
true
|
329
387
|
end
|
330
|
-
output.join("\n")
|
331
388
|
end
|
332
389
|
|
333
390
|
|
@@ -480,7 +537,8 @@ module AIA
|
|
480
537
|
chat_instance.ask(text_prompt, with: AIA.config.context_files)
|
481
538
|
end
|
482
539
|
|
483
|
-
response
|
540
|
+
# Return the full response object to preserve token information
|
541
|
+
response
|
484
542
|
rescue StandardError => e
|
485
543
|
e.message
|
486
544
|
end
|
data/lib/aia/session.rb
CHANGED
@@ -372,11 +372,34 @@ module AIA
|
|
372
372
|
conversation = @context_manager.get_context
|
373
373
|
|
374
374
|
@ui_presenter.display_thinking_animation
|
375
|
-
|
375
|
+
response_data = @chat_processor.process_prompt(conversation)
|
376
|
+
|
377
|
+
# Handle new response format with metrics
|
378
|
+
if response_data.is_a?(Hash)
|
379
|
+
content = response_data[:content]
|
380
|
+
metrics = response_data[:metrics]
|
381
|
+
multi_metrics = response_data[:multi_metrics]
|
382
|
+
else
|
383
|
+
content = response_data
|
384
|
+
metrics = nil
|
385
|
+
multi_metrics = nil
|
386
|
+
end
|
376
387
|
|
377
|
-
@ui_presenter.display_ai_response(
|
378
|
-
|
379
|
-
|
388
|
+
@ui_presenter.display_ai_response(content)
|
389
|
+
|
390
|
+
# Display metrics if enabled and available (chat mode only)
|
391
|
+
if AIA.config.show_metrics
|
392
|
+
if multi_metrics
|
393
|
+
# Display metrics for each model in multi-model mode
|
394
|
+
@ui_presenter.display_multi_model_metrics(multi_metrics)
|
395
|
+
elsif metrics
|
396
|
+
# Display metrics for single model
|
397
|
+
@ui_presenter.display_token_metrics(metrics)
|
398
|
+
end
|
399
|
+
end
|
400
|
+
|
401
|
+
@context_manager.add_to_context(role: "assistant", content: content)
|
402
|
+
@chat_processor.speak(content)
|
380
403
|
|
381
404
|
@ui_presenter.display_separator
|
382
405
|
end
|
data/lib/aia/ui_presenter.rb
CHANGED
@@ -117,5 +117,211 @@ module AIA
|
|
117
117
|
yield
|
118
118
|
end
|
119
119
|
end
|
120
|
+
|
121
|
+
def display_token_metrics(metrics)
|
122
|
+
return unless metrics
|
123
|
+
|
124
|
+
output_lines = []
|
125
|
+
output_lines << "═" * 55
|
126
|
+
output_lines << "Model: #{metrics[:model_id]}"
|
127
|
+
|
128
|
+
if AIA.config.show_cost
|
129
|
+
output_lines.concat(format_metrics_with_cost(metrics))
|
130
|
+
else
|
131
|
+
output_lines.concat(format_metrics_basic(metrics))
|
132
|
+
end
|
133
|
+
|
134
|
+
output_lines << "═" * 55
|
135
|
+
|
136
|
+
# Output to STDOUT
|
137
|
+
output_lines.each { |line| puts line }
|
138
|
+
|
139
|
+
# Also write to file if configured
|
140
|
+
if AIA.config.out_file && !AIA.config.out_file.nil?
|
141
|
+
File.open(AIA.config.out_file, 'a') do |file|
|
142
|
+
output_lines.each { |line| file.puts line }
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def display_multi_model_metrics(metrics_list)
|
148
|
+
return unless metrics_list && !metrics_list.empty?
|
149
|
+
|
150
|
+
output_lines = []
|
151
|
+
|
152
|
+
# Determine table width based on whether costs are shown
|
153
|
+
if AIA.config.show_cost
|
154
|
+
table_width = 80
|
155
|
+
else
|
156
|
+
table_width = 60
|
157
|
+
end
|
158
|
+
|
159
|
+
output_lines << "═" * table_width
|
160
|
+
output_lines << "Multi-Model Token Usage"
|
161
|
+
output_lines << "─" * table_width
|
162
|
+
|
163
|
+
# Build header row
|
164
|
+
if AIA.config.show_cost
|
165
|
+
output_lines << sprintf("%-20s %10s %10s %10s %12s %10s",
|
166
|
+
"Model", "Input", "Output", "Total", "Cost", "x1000")
|
167
|
+
output_lines << "─" * table_width
|
168
|
+
else
|
169
|
+
output_lines << sprintf("%-20s %10s %10s %10s",
|
170
|
+
"Model", "Input", "Output", "Total")
|
171
|
+
output_lines << "─" * table_width
|
172
|
+
end
|
173
|
+
|
174
|
+
# Process each model
|
175
|
+
total_input = 0
|
176
|
+
total_output = 0
|
177
|
+
total_cost = 0.0
|
178
|
+
|
179
|
+
metrics_list.each do |metrics|
|
180
|
+
model_name = metrics[:model_id]
|
181
|
+
# Truncate model name if too long
|
182
|
+
model_name = model_name[0..17] + ".." if model_name.length > 19
|
183
|
+
|
184
|
+
input_tokens = metrics[:input_tokens] || 0
|
185
|
+
output_tokens = metrics[:output_tokens] || 0
|
186
|
+
total_tokens = input_tokens + output_tokens
|
187
|
+
|
188
|
+
if AIA.config.show_cost
|
189
|
+
cost_data = calculate_cost(metrics)
|
190
|
+
if cost_data[:available]
|
191
|
+
cost_str = "$#{'%.5f' % cost_data[:total_cost]}"
|
192
|
+
x1000_str = "$#{'%.2f' % (cost_data[:total_cost] * 1000)}"
|
193
|
+
total_cost += cost_data[:total_cost]
|
194
|
+
else
|
195
|
+
cost_str = "N/A"
|
196
|
+
x1000_str = "N/A"
|
197
|
+
end
|
198
|
+
|
199
|
+
output_lines << sprintf("%-20s %10d %10d %10d %12s %10s",
|
200
|
+
model_name, input_tokens, output_tokens, total_tokens, cost_str, x1000_str)
|
201
|
+
else
|
202
|
+
output_lines << sprintf("%-20s %10d %10d %10d",
|
203
|
+
model_name, input_tokens, output_tokens, total_tokens)
|
204
|
+
end
|
205
|
+
|
206
|
+
total_input += input_tokens
|
207
|
+
total_output += output_tokens
|
208
|
+
end
|
209
|
+
|
210
|
+
# Display totals row
|
211
|
+
output_lines << "─" * table_width
|
212
|
+
total_tokens = total_input + total_output
|
213
|
+
|
214
|
+
if AIA.config.show_cost && total_cost > 0
|
215
|
+
cost_str = "$#{'%.5f' % total_cost}"
|
216
|
+
x1000_str = "$#{'%.2f' % (total_cost * 1000)}"
|
217
|
+
output_lines << sprintf("%-20s %10d %10d %10d %12s %10s",
|
218
|
+
"TOTAL", total_input, total_output, total_tokens, cost_str, x1000_str)
|
219
|
+
else
|
220
|
+
output_lines << sprintf("%-20s %10d %10d %10d",
|
221
|
+
"TOTAL", total_input, total_output, total_tokens)
|
222
|
+
end
|
223
|
+
|
224
|
+
output_lines << "═" * table_width
|
225
|
+
|
226
|
+
# Output to STDOUT
|
227
|
+
output_lines.each { |line| puts line }
|
228
|
+
|
229
|
+
# Also write to file if configured
|
230
|
+
if AIA.config.out_file && !AIA.config.out_file.nil?
|
231
|
+
File.open(AIA.config.out_file, 'a') do |file|
|
232
|
+
output_lines.each { |line| file.puts line }
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
private
|
238
|
+
|
239
|
+
def display_metrics_basic(metrics)
|
240
|
+
puts "Input tokens: #{metrics[:input_tokens] || 'N/A'}"
|
241
|
+
puts "Output tokens: #{metrics[:output_tokens] || 'N/A'}"
|
242
|
+
|
243
|
+
if metrics[:input_tokens] && metrics[:output_tokens]
|
244
|
+
total = metrics[:input_tokens] + metrics[:output_tokens]
|
245
|
+
puts "Total tokens: #{total}"
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
def format_metrics_basic(metrics)
|
250
|
+
lines = []
|
251
|
+
lines << "Input tokens: #{metrics[:input_tokens] || 'N/A'}"
|
252
|
+
lines << "Output tokens: #{metrics[:output_tokens] || 'N/A'}"
|
253
|
+
|
254
|
+
if metrics[:input_tokens] && metrics[:output_tokens]
|
255
|
+
total = metrics[:input_tokens] + metrics[:output_tokens]
|
256
|
+
lines << "Total tokens: #{total}"
|
257
|
+
end
|
258
|
+
|
259
|
+
lines
|
260
|
+
end
|
261
|
+
|
262
|
+
def display_metrics_with_cost(metrics)
|
263
|
+
cost_data = calculate_cost(metrics)
|
264
|
+
|
265
|
+
if cost_data[:available]
|
266
|
+
puts "Input tokens: #{metrics[:input_tokens]} ($#{'%.5f' % cost_data[:input_cost]})"
|
267
|
+
puts "Output tokens: #{metrics[:output_tokens]} ($#{'%.5f' % cost_data[:output_cost]})"
|
268
|
+
puts "Total cost: $#{'%.5f' % cost_data[:total_cost]}"
|
269
|
+
puts "Cost x1000: $#{'%.2f' % (cost_data[:total_cost] * 1000)}"
|
270
|
+
else
|
271
|
+
puts "Input tokens: #{metrics[:input_tokens] || 'N/A'}"
|
272
|
+
puts "Output tokens: #{metrics[:output_tokens] || 'N/A'}"
|
273
|
+
total = (metrics[:input_tokens] || 0) + (metrics[:output_tokens] || 0)
|
274
|
+
puts "Total tokens: #{total}"
|
275
|
+
puts "Cost: N/A (pricing not available)"
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
def format_metrics_with_cost(metrics)
|
280
|
+
lines = []
|
281
|
+
cost_data = calculate_cost(metrics)
|
282
|
+
|
283
|
+
if cost_data[:available]
|
284
|
+
lines << "Input tokens: #{metrics[:input_tokens]} ($#{'%.5f' % cost_data[:input_cost]})"
|
285
|
+
lines << "Output tokens: #{metrics[:output_tokens]} ($#{'%.5f' % cost_data[:output_cost]})"
|
286
|
+
lines << "Total cost: $#{'%.5f' % cost_data[:total_cost]}"
|
287
|
+
lines << "Cost x1000: $#{'%.2f' % (cost_data[:total_cost] * 1000)}"
|
288
|
+
else
|
289
|
+
lines << "Input tokens: #{metrics[:input_tokens] || 'N/A'}"
|
290
|
+
lines << "Output tokens: #{metrics[:output_tokens] || 'N/A'}"
|
291
|
+
total = (metrics[:input_tokens] || 0) + (metrics[:output_tokens] || 0)
|
292
|
+
lines << "Total tokens: #{total}"
|
293
|
+
lines << "Cost: N/A (pricing not available)"
|
294
|
+
end
|
295
|
+
|
296
|
+
lines
|
297
|
+
end
|
298
|
+
|
299
|
+
def calculate_cost(metrics)
|
300
|
+
return { available: false } unless metrics[:model_id] && metrics[:input_tokens] && metrics[:output_tokens]
|
301
|
+
|
302
|
+
# Look up model info from RubyLLM
|
303
|
+
begin
|
304
|
+
model_info = RubyLLM::Models.find(metrics[:model_id])
|
305
|
+
return { available: false } unless model_info
|
306
|
+
|
307
|
+
input_price = model_info.input_price_per_million
|
308
|
+
output_price = model_info.output_price_per_million
|
309
|
+
|
310
|
+
return { available: false } unless input_price && output_price
|
311
|
+
|
312
|
+
input_cost = metrics[:input_tokens] * input_price / 1_000_000.0
|
313
|
+
output_cost = metrics[:output_tokens] * output_price / 1_000_000.0
|
314
|
+
total_cost = input_cost + output_cost
|
315
|
+
|
316
|
+
{
|
317
|
+
available: true,
|
318
|
+
input_cost: input_cost,
|
319
|
+
output_cost: output_cost,
|
320
|
+
total_cost: total_cost
|
321
|
+
}
|
322
|
+
rescue StandardError => e
|
323
|
+
{ available: false, error: e.message }
|
324
|
+
end
|
325
|
+
end
|
120
326
|
end
|
121
327
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: aia
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.9.
|
4
|
+
version: 0.9.13
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dewayne VanHoozer
|
@@ -71,14 +71,14 @@ dependencies:
|
|
71
71
|
requirements:
|
72
72
|
- - ">="
|
73
73
|
- !ruby/object:Gem::Version
|
74
|
-
version: 0.5.
|
74
|
+
version: 0.5.8
|
75
75
|
type: :runtime
|
76
76
|
prerelease: false
|
77
77
|
version_requirements: !ruby/object:Gem::Requirement
|
78
78
|
requirements:
|
79
79
|
- - ">="
|
80
80
|
- !ruby/object:Gem::Version
|
81
|
-
version: 0.5.
|
81
|
+
version: 0.5.8
|
82
82
|
- !ruby/object:Gem::Dependency
|
83
83
|
name: ruby_llm
|
84
84
|
requirement: !ruby/object:Gem::Requirement
|
@@ -177,20 +177,6 @@ dependencies:
|
|
177
177
|
- - ">="
|
178
178
|
- !ruby/object:Gem::Version
|
179
179
|
version: '0'
|
180
|
-
- !ruby/object:Gem::Dependency
|
181
|
-
name: versionaire
|
182
|
-
requirement: !ruby/object:Gem::Requirement
|
183
|
-
requirements:
|
184
|
-
- - ">="
|
185
|
-
- !ruby/object:Gem::Version
|
186
|
-
version: '0'
|
187
|
-
type: :runtime
|
188
|
-
prerelease: false
|
189
|
-
version_requirements: !ruby/object:Gem::Requirement
|
190
|
-
requirements:
|
191
|
-
- - ">="
|
192
|
-
- !ruby/object:Gem::Version
|
193
|
-
version: '0'
|
194
180
|
- !ruby/object:Gem::Dependency
|
195
181
|
name: word_wrapper
|
196
182
|
requirement: !ruby/object:Gem::Requirement
|
@@ -329,7 +315,6 @@ files:
|
|
329
315
|
- LICENSE
|
330
316
|
- README.md
|
331
317
|
- Rakefile
|
332
|
-
- _notes.txt
|
333
318
|
- bin/aia
|
334
319
|
- docs/advanced-prompting.md
|
335
320
|
- docs/assets/images/aia.png
|
data/_notes.txt
DELETED
@@ -1,231 +0,0 @@
|
|
1
|
-
|
2
|
-
--- 2025-02-01 18:01:36 -0600
|
3
|
-
I have no idea where I left off in this branch. The objective is to replace all the back-end processes with AiClient.
|
4
|
-
|
5
|
-
Tests are failing.
|
6
|
-
|
7
|
-
Make a few changes. It seems to be working in its basic modes.
|
8
|
-
|
9
|
-
--- 2025-02-21 20:13:19 -0600
|
10
|
-
Implemented Stark's clean slate protocol
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
--- 2025-03-29 21:39:46 -0500
|
17
|
-
starting the refactor to take advantage of the new capability of the PromptMananger gem.
|
18
|
-
|
19
|
-
lib/aia/chat_processor_service.rb
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
--- 2025-04-03 22:17:11 -0500
|
24
|
-
i have been tring to get multi-line input to work in the chat mode but have run into all kinds of problems. I think it would be best just to invoke the users editor for that kind of operation. Alo I am not sure but I thing the same ask method is used for getting values for parameters. changes may have been committed but they should be reversed back to the original and start over.
|
25
|
-
|
26
|
-
def get_multiline_input
|
27
|
-
input_lines = []
|
28
|
-
current_line = ""
|
29
|
-
last_key_time = Time.now
|
30
|
-
waiting_printed = 0 # Track number of WAITING characters printed
|
31
|
-
|
32
|
-
STDIN.raw! # Enable raw mode for immediate keypress detection
|
33
|
-
begin
|
34
|
-
loop do
|
35
|
-
begin
|
36
|
-
r, _, _ = IO.select([STDIN], nil, nil, 0.1)
|
37
|
-
if r
|
38
|
-
char = STDIN.getc
|
39
|
-
last_key_time = Time.now
|
40
|
-
# Clear waiting characters when user types again
|
41
|
-
if waiting_printed > 0
|
42
|
-
print WAITING_ERASE * waiting_printed # Erase all waiting characters
|
43
|
-
$stdout.flush
|
44
|
-
waiting_printed = 0
|
45
|
-
end
|
46
|
-
else
|
47
|
-
if (Time.now - last_key_time >= KEYPRESS_TIMEUT) &&
|
48
|
-
waiting_printed == 0 &&
|
49
|
-
(!input_lines.empty? || !current_line.empty?)
|
50
|
-
print WAITING
|
51
|
-
$stdout.flush
|
52
|
-
waiting_printed = 1 # Record one '?' printed
|
53
|
-
end
|
54
|
-
next
|
55
|
-
end
|
56
|
-
|
57
|
-
rescue Interrupt
|
58
|
-
puts "\nInput cancelled. Discarding current input; please start over."
|
59
|
-
input_lines = []
|
60
|
-
current_line = ""
|
61
|
-
waiting_printed = 0
|
62
|
-
last_key_time = Time.now
|
63
|
-
next
|
64
|
-
end
|
65
|
-
|
66
|
-
break if char.nil? # Handle EOF (Ctrl+D)
|
67
|
-
|
68
|
-
if char == "\r" || char == "\n"
|
69
|
-
if current_line.empty? && !input_lines.empty?
|
70
|
-
break # Two Enters in a row submits
|
71
|
-
else
|
72
|
-
input_lines << current_line
|
73
|
-
current_line = ""
|
74
|
-
waiting_printed = 0 # Reset waiting on new line
|
75
|
-
print "\n\r"
|
76
|
-
$stdout.flush
|
77
|
-
end
|
78
|
-
|
79
|
-
elsif char == "\x04" # Ctrl+D
|
80
|
-
break
|
81
|
-
|
82
|
-
elsif char == "\x08" || char == "\x7F" # Backspace or Delete
|
83
|
-
if !current_line.empty?
|
84
|
-
current_line.chop!
|
85
|
-
print WAITING_ERASE
|
86
|
-
$stdout.flush
|
87
|
-
elsif waiting_printed > 0
|
88
|
-
# Clear one waiting character if current_line is empty
|
89
|
-
print "\b \b"
|
90
|
-
$stdout.flush
|
91
|
-
waiting_printed -= 1
|
92
|
-
end
|
93
|
-
|
94
|
-
else
|
95
|
-
current_line << char
|
96
|
-
print char
|
97
|
-
$stdout.flush
|
98
|
-
end
|
99
|
-
end
|
100
|
-
|
101
|
-
ensure
|
102
|
-
STDIN.cooked! # Restore terminal to normal mode
|
103
|
-
end
|
104
|
-
|
105
|
-
input_lines << current_line unless current_line.empty?
|
106
|
-
|
107
|
-
# Handle single-line special case
|
108
|
-
if input_lines.size == 1
|
109
|
-
if special_first_line_processing(input_lines.first)
|
110
|
-
# If special (starts with "//"), return immediately as if double return was pressed
|
111
|
-
return input_lines.first
|
112
|
-
else
|
113
|
-
# If not special, keep as is and return the full input
|
114
|
-
return input_lines.join("\n")
|
115
|
-
end
|
116
|
-
end
|
117
|
-
|
118
|
-
input_lines.join("\n").tap do |result|
|
119
|
-
puts "\n" if result.empty? # Clean up display if no input
|
120
|
-
end
|
121
|
-
|
122
|
-
rescue EOFError
|
123
|
-
input_lines.join("\n")
|
124
|
-
end
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
--- 2025-04-03 22:18:18 -0500
|
129
|
-
by using subl -w for multi-line input in chat mode that gives us the ability to write ERB for chat input.
|
130
|
-
|
131
|
-
def get_multiline_input
|
132
|
-
input_lines = []
|
133
|
-
current_line = ""
|
134
|
-
last_key_time = Time.now
|
135
|
-
waiting_printed = 0 # Track number of WAITING characters printed
|
136
|
-
|
137
|
-
STDIN.raw! # Enable raw mode for immediate keypress detection
|
138
|
-
begin
|
139
|
-
loop do
|
140
|
-
begin
|
141
|
-
r, _, _ = IO.select([STDIN], nil, nil, 0.1)
|
142
|
-
if r
|
143
|
-
char = STDIN.getc
|
144
|
-
last_key_time = Time.now
|
145
|
-
# Clear waiting characters when user types again
|
146
|
-
if waiting_printed > 0
|
147
|
-
print WAITING_ERASE * waiting_printed # Erase all waiting characters
|
148
|
-
$stdout.flush
|
149
|
-
waiting_printed = 0
|
150
|
-
end
|
151
|
-
else
|
152
|
-
if (Time.now - last_key_time >= KEYPRESS_TIMEUT) &&
|
153
|
-
waiting_printed == 0 &&
|
154
|
-
(!input_lines.empty? || !current_line.empty?)
|
155
|
-
print WAITING
|
156
|
-
$stdout.flush
|
157
|
-
waiting_printed = 1 # Record one '?' printed
|
158
|
-
end
|
159
|
-
next
|
160
|
-
end
|
161
|
-
|
162
|
-
rescue Interrupt
|
163
|
-
puts "\nInput cancelled. Discarding current input; please start over."
|
164
|
-
input_lines = []
|
165
|
-
current_line = ""
|
166
|
-
waiting_printed = 0
|
167
|
-
last_key_time = Time.now
|
168
|
-
next
|
169
|
-
end
|
170
|
-
|
171
|
-
break if char.nil? # Handle EOF (Ctrl+D)
|
172
|
-
|
173
|
-
if char == "\r" || char == "\n"
|
174
|
-
if current_line.empty? && !input_lines.empty?
|
175
|
-
break # Two Enters in a row submits
|
176
|
-
else
|
177
|
-
input_lines << current_line
|
178
|
-
current_line = ""
|
179
|
-
waiting_printed = 0 # Reset waiting on new line
|
180
|
-
print "\n\r"
|
181
|
-
$stdout.flush
|
182
|
-
end
|
183
|
-
|
184
|
-
elsif char == "\x04" # Ctrl+D
|
185
|
-
break
|
186
|
-
|
187
|
-
elsif char == "\x08" || char == "\x7F" # Backspace or Delete
|
188
|
-
if !current_line.empty?
|
189
|
-
current_line.chop!
|
190
|
-
print WAITING_ERASE
|
191
|
-
$stdout.flush
|
192
|
-
elsif waiting_printed > 0
|
193
|
-
# Clear one waiting character if current_line is empty
|
194
|
-
print "\b \b"
|
195
|
-
$stdout.flush
|
196
|
-
waiting_printed -= 1
|
197
|
-
end
|
198
|
-
|
199
|
-
else
|
200
|
-
current_line << char
|
201
|
-
print char
|
202
|
-
$stdout.flush
|
203
|
-
end
|
204
|
-
end
|
205
|
-
|
206
|
-
ensure
|
207
|
-
STDIN.cooked! # Restore terminal to normal mode
|
208
|
-
end
|
209
|
-
|
210
|
-
input_lines << current_line unless current_line.empty?
|
211
|
-
|
212
|
-
# Handle single-line special case
|
213
|
-
if input_lines.size == 1
|
214
|
-
if special_first_line_processing(input_lines.first)
|
215
|
-
# If special (starts with "//"), return immediately as if double return was pressed
|
216
|
-
return input_lines.first
|
217
|
-
else
|
218
|
-
# If not special, keep as is and return the full input
|
219
|
-
return input_lines.join("\n")
|
220
|
-
end
|
221
|
-
end
|
222
|
-
|
223
|
-
input_lines.join("\n").tap do |result|
|
224
|
-
puts "\n" if result.empty? # Clean up display if no input
|
225
|
-
end
|
226
|
-
|
227
|
-
rescue EOFError
|
228
|
-
input_lines.join("\n")
|
229
|
-
end
|
230
|
-
|
231
|
-
|