legion-tty 0.4.11 → 0.4.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/CHANGELOG.md +23 -0
- data/README.md +47 -21
- data/lib/legion/tty/components/status_bar.rb +9 -1
- data/lib/legion/tty/screens/chat/custom_commands.rb +148 -0
- data/lib/legion/tty/screens/chat/export_commands.rb +125 -0
- data/lib/legion/tty/screens/chat/message_commands.rb +192 -0
- data/lib/legion/tty/screens/chat/model_commands.rb +123 -0
- data/lib/legion/tty/screens/chat/session_commands.rb +93 -0
- data/lib/legion/tty/screens/chat/ui_commands.rb +238 -0
- data/lib/legion/tty/screens/chat.rb +49 -799
- data/lib/legion/tty/version.rb +1 -1
- metadata +7 -1
|
@@ -6,16 +6,29 @@ require_relative '../components/status_bar'
|
|
|
6
6
|
require_relative '../components/input_bar'
|
|
7
7
|
require_relative '../components/token_tracker'
|
|
8
8
|
require_relative '../theme'
|
|
9
|
+
require_relative 'chat/session_commands'
|
|
10
|
+
require_relative 'chat/export_commands'
|
|
11
|
+
require_relative 'chat/message_commands'
|
|
12
|
+
require_relative 'chat/ui_commands'
|
|
13
|
+
require_relative 'chat/model_commands'
|
|
14
|
+
require_relative 'chat/custom_commands'
|
|
9
15
|
|
|
10
16
|
module Legion
|
|
11
17
|
module TTY
|
|
12
18
|
module Screens
|
|
13
19
|
# rubocop:disable Metrics/ClassLength
|
|
14
20
|
class Chat < Base
|
|
21
|
+
include SessionCommands
|
|
22
|
+
include ExportCommands
|
|
23
|
+
include MessageCommands
|
|
24
|
+
include UiCommands
|
|
25
|
+
include ModelCommands
|
|
26
|
+
include CustomCommands
|
|
27
|
+
|
|
15
28
|
SLASH_COMMANDS = %w[/help /quit /clear /compact /copy /diff /model /session /cost /export /tools /dashboard
|
|
16
29
|
/hotkeys /save /load /sessions /system /delete /plan /palette /extensions /config
|
|
17
|
-
/theme /search /stats /personality /undo /history /pin /pins /rename
|
|
18
|
-
/context /alias /snippet /debug /uptime /bookmark].freeze
|
|
30
|
+
/theme /search /grep /stats /personality /undo /history /pin /pins /rename
|
|
31
|
+
/context /alias /snippet /debug /uptime /time /bookmark /welcome /tips].freeze
|
|
19
32
|
|
|
20
33
|
PERSONALITIES = {
|
|
21
34
|
'default' => 'You are Legion, an async cognition engine and AI assistant. Be helpful and concise.',
|
|
@@ -58,6 +71,7 @@ module Legion
|
|
|
58
71
|
role: :system,
|
|
59
72
|
content: "Welcome#{", #{cfg[:name]}" if cfg[:name]}. Type /help for commands."
|
|
60
73
|
)
|
|
74
|
+
@status_bar.update(message_count: @message_stream.messages.size)
|
|
61
75
|
end
|
|
62
76
|
|
|
63
77
|
def running?
|
|
@@ -111,6 +125,7 @@ module Legion
|
|
|
111
125
|
@message_stream.add_message(role: :assistant, content: '')
|
|
112
126
|
send_to_llm(input)
|
|
113
127
|
end
|
|
128
|
+
@status_bar.update(message_count: @message_stream.messages.size)
|
|
114
129
|
render_screen
|
|
115
130
|
end
|
|
116
131
|
|
|
@@ -308,6 +323,7 @@ module Legion
|
|
|
308
323
|
when '/config' then handle_config_screen
|
|
309
324
|
when '/theme' then handle_theme(input)
|
|
310
325
|
when '/search' then handle_search(input)
|
|
326
|
+
when '/grep' then handle_grep(input)
|
|
311
327
|
when '/stats' then handle_stats
|
|
312
328
|
when '/personality' then handle_personality(input)
|
|
313
329
|
when '/undo' then handle_undo
|
|
@@ -320,216 +336,20 @@ module Legion
|
|
|
320
336
|
when '/snippet' then handle_snippet(input)
|
|
321
337
|
when '/debug' then handle_debug
|
|
322
338
|
when '/uptime' then handle_uptime
|
|
339
|
+
when '/time' then handle_time
|
|
323
340
|
when '/bookmark' then handle_bookmark
|
|
341
|
+
when '/welcome' then handle_welcome
|
|
342
|
+
when '/tips' then handle_tips
|
|
324
343
|
else :handled
|
|
325
344
|
end
|
|
326
345
|
end
|
|
327
346
|
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
328
347
|
|
|
329
|
-
# rubocop:disable Metrics/MethodLength
|
|
330
|
-
def handle_help
|
|
331
|
-
@message_stream.add_message(
|
|
332
|
-
role: :system,
|
|
333
|
-
content: "Commands:\n /help /quit /clear /model <name> /session <name> /cost\n " \
|
|
334
|
-
"/export [md|json] /tools /dashboard /hotkeys /save /load /sessions\n " \
|
|
335
|
-
"/system <prompt> /delete <session> /plan /palette /extensions /config\n " \
|
|
336
|
-
"/theme [name] -- switch color theme (purple, green, blue, amber)\n " \
|
|
337
|
-
"/search <text> -- search message history\n " \
|
|
338
|
-
"/compact [n] -- keep last n message pairs (default 5)\n " \
|
|
339
|
-
"/copy -- copy last assistant message to clipboard\n " \
|
|
340
|
-
"/diff -- show new messages since session was loaded\n " \
|
|
341
|
-
"/stats -- show conversation statistics\n " \
|
|
342
|
-
"/personality [name] -- switch assistant personality\n " \
|
|
343
|
-
"/undo -- remove last user+assistant message pair\n " \
|
|
344
|
-
"/history -- show recent input history\n " \
|
|
345
|
-
"/pin [N] -- pin last assistant message (or message at index N)\n " \
|
|
346
|
-
"/pins -- show all pinned messages\n " \
|
|
347
|
-
"/rename <name> -- rename current session\n " \
|
|
348
|
-
"/context -- show active session context summary\n " \
|
|
349
|
-
"/alias [shortname /command] -- create or list command aliases\n " \
|
|
350
|
-
"/snippet save|load|list|delete <name> -- manage reusable text snippets\n " \
|
|
351
|
-
"/debug -- toggle debug mode (shows internal state)\n " \
|
|
352
|
-
"/uptime -- show how long this session has been active\n " \
|
|
353
|
-
"/bookmark -- export pinned messages to a markdown file\n\n" \
|
|
354
|
-
'Hotkeys: Ctrl+D=dashboard Ctrl+K=palette Ctrl+S=sessions Esc=back'
|
|
355
|
-
)
|
|
356
|
-
:handled
|
|
357
|
-
end
|
|
358
|
-
# rubocop:enable Metrics/MethodLength
|
|
359
|
-
|
|
360
|
-
def handle_clear
|
|
361
|
-
@message_stream.messages.clear
|
|
362
|
-
:handled
|
|
363
|
-
end
|
|
364
|
-
|
|
365
|
-
def handle_model(input)
|
|
366
|
-
name = input.split(nil, 2)[1]
|
|
367
|
-
if name
|
|
368
|
-
switch_model(name)
|
|
369
|
-
else
|
|
370
|
-
show_current_model
|
|
371
|
-
end
|
|
372
|
-
:handled
|
|
373
|
-
end
|
|
374
|
-
|
|
375
|
-
def switch_model(name)
|
|
376
|
-
unless @llm_chat
|
|
377
|
-
@message_stream.add_message(role: :system, content: 'No active LLM session.')
|
|
378
|
-
return
|
|
379
|
-
end
|
|
380
|
-
|
|
381
|
-
apply_model_switch(name)
|
|
382
|
-
rescue StandardError => e
|
|
383
|
-
@message_stream.add_message(role: :system, content: "Failed to switch model: #{e.message}")
|
|
384
|
-
end
|
|
385
|
-
|
|
386
|
-
def apply_model_switch(name)
|
|
387
|
-
new_chat = try_provider_switch(name)
|
|
388
|
-
if new_chat
|
|
389
|
-
@llm_chat = new_chat
|
|
390
|
-
@status_bar.update(model: name)
|
|
391
|
-
@token_tracker.update_model(name)
|
|
392
|
-
@message_stream.add_message(role: :system, content: "Switched to provider: #{name}")
|
|
393
|
-
elsif @llm_chat.respond_to?(:with_model)
|
|
394
|
-
@llm_chat.with_model(name)
|
|
395
|
-
@status_bar.update(model: name)
|
|
396
|
-
@token_tracker.update_model(name)
|
|
397
|
-
@message_stream.add_message(role: :system, content: "Model switched to: #{name}")
|
|
398
|
-
else
|
|
399
|
-
@status_bar.update(model: name)
|
|
400
|
-
@message_stream.add_message(role: :system, content: "Model set to: #{name}")
|
|
401
|
-
end
|
|
402
|
-
end
|
|
403
|
-
|
|
404
|
-
def try_provider_switch(name)
|
|
405
|
-
return nil unless defined?(Legion::LLM)
|
|
406
|
-
|
|
407
|
-
providers = Legion::LLM.settings[:providers]
|
|
408
|
-
return nil unless providers.is_a?(Hash) && providers.key?(name.to_sym)
|
|
409
|
-
|
|
410
|
-
Legion::LLM.chat(provider: name)
|
|
411
|
-
rescue StandardError
|
|
412
|
-
nil
|
|
413
|
-
end
|
|
414
|
-
|
|
415
|
-
def open_model_picker
|
|
416
|
-
require_relative '../components/model_picker'
|
|
417
|
-
picker = Components::ModelPicker.new(
|
|
418
|
-
current_provider: safe_config[:provider],
|
|
419
|
-
current_model: @llm_chat.respond_to?(:model) ? @llm_chat.model.to_s : nil
|
|
420
|
-
)
|
|
421
|
-
selection = picker.select_with_prompt(output: @output)
|
|
422
|
-
return unless selection
|
|
423
|
-
|
|
424
|
-
switch_model(selection[:provider])
|
|
425
|
-
end
|
|
426
|
-
|
|
427
|
-
def show_current_model
|
|
428
|
-
model = @llm_chat.respond_to?(:model) ? @llm_chat.model : nil
|
|
429
|
-
provider = safe_config[:provider] || 'unknown'
|
|
430
|
-
info = model ? "#{model} (#{provider})" : provider
|
|
431
|
-
@message_stream.add_message(role: :system, content: "Current model: #{info}")
|
|
432
|
-
end
|
|
433
|
-
|
|
434
|
-
def handle_session(input)
|
|
435
|
-
name = input.split(nil, 2)[1]
|
|
436
|
-
if name
|
|
437
|
-
@session_name = name
|
|
438
|
-
@status_bar.update(session: name)
|
|
439
|
-
end
|
|
440
|
-
:handled
|
|
441
|
-
end
|
|
442
|
-
|
|
443
|
-
def handle_save(input)
|
|
444
|
-
name = input.split(nil, 2)[1] || @session_store.auto_session_name
|
|
445
|
-
@session_name = name
|
|
446
|
-
@session_store.save(name, messages: @message_stream.messages)
|
|
447
|
-
@status_bar.update(session: name)
|
|
448
|
-
@status_bar.notify(message: "Saved '#{name}'", level: :success, ttl: 3)
|
|
449
|
-
@message_stream.add_message(role: :system, content: "Session saved as '#{name}'.")
|
|
450
|
-
:handled
|
|
451
|
-
end
|
|
452
|
-
|
|
453
|
-
def handle_load(input)
|
|
454
|
-
name = input.split(nil, 2)[1]
|
|
455
|
-
unless name
|
|
456
|
-
@message_stream.add_message(role: :system, content: 'Usage: /load <session-name>')
|
|
457
|
-
return :handled
|
|
458
|
-
end
|
|
459
|
-
data = @session_store.load(name)
|
|
460
|
-
unless data
|
|
461
|
-
@message_stream.add_message(role: :system, content: "Session '#{name}' not found.")
|
|
462
|
-
return :handled
|
|
463
|
-
end
|
|
464
|
-
@message_stream.messages.replace(data[:messages])
|
|
465
|
-
@loaded_message_count = @message_stream.messages.size
|
|
466
|
-
@session_name = name
|
|
467
|
-
@status_bar.update(session: name)
|
|
468
|
-
@status_bar.notify(message: "Loaded '#{name}'", level: :info, ttl: 3)
|
|
469
|
-
@message_stream.add_message(role: :system,
|
|
470
|
-
content: "Session '#{name}' loaded (#{data[:messages].size} messages).")
|
|
471
|
-
:handled
|
|
472
|
-
end
|
|
473
|
-
|
|
474
|
-
def handle_sessions
|
|
475
|
-
sessions = @session_store.list
|
|
476
|
-
if sessions.empty?
|
|
477
|
-
@message_stream.add_message(role: :system, content: 'No saved sessions.')
|
|
478
|
-
else
|
|
479
|
-
lines = sessions.map { |s| " #{s[:name]} - #{s[:message_count]} messages (#{s[:saved_at]})" }
|
|
480
|
-
@message_stream.add_message(role: :system, content: "Saved sessions:\n#{lines.join("\n")}")
|
|
481
|
-
end
|
|
482
|
-
:handled
|
|
483
|
-
end
|
|
484
|
-
|
|
485
|
-
def auto_save_session
|
|
486
|
-
return if @message_stream.messages.empty?
|
|
487
|
-
|
|
488
|
-
if @session_name == 'default'
|
|
489
|
-
@session_name = @session_store.auto_session_name(messages: @message_stream.messages)
|
|
490
|
-
end
|
|
491
|
-
@session_store.save(@session_name, messages: @message_stream.messages)
|
|
492
|
-
rescue StandardError
|
|
493
|
-
nil
|
|
494
|
-
end
|
|
495
|
-
|
|
496
348
|
def handle_cost
|
|
497
349
|
@message_stream.add_message(role: :system, content: @token_tracker.summary)
|
|
498
350
|
:handled
|
|
499
351
|
end
|
|
500
352
|
|
|
501
|
-
def handle_export(input)
|
|
502
|
-
require 'fileutils'
|
|
503
|
-
path = build_export_path(input)
|
|
504
|
-
dispatch_export(path, input.split[1]&.downcase)
|
|
505
|
-
@status_bar.notify(message: 'Exported', level: :success, ttl: 3)
|
|
506
|
-
@message_stream.add_message(role: :system, content: "Exported to: #{path}")
|
|
507
|
-
:handled
|
|
508
|
-
rescue StandardError => e
|
|
509
|
-
@message_stream.add_message(role: :system, content: "Export failed: #{e.message}")
|
|
510
|
-
:handled
|
|
511
|
-
end
|
|
512
|
-
|
|
513
|
-
def build_export_path(input)
|
|
514
|
-
format = input.split[1]&.downcase
|
|
515
|
-
format = 'md' unless %w[json md html].include?(format)
|
|
516
|
-
exports_dir = File.expand_path('~/.legionio/exports')
|
|
517
|
-
FileUtils.mkdir_p(exports_dir)
|
|
518
|
-
timestamp = Time.now.strftime('%Y%m%d-%H%M%S')
|
|
519
|
-
ext = { 'json' => 'json', 'md' => 'md', 'html' => 'html' }[format]
|
|
520
|
-
File.join(exports_dir, "chat-#{timestamp}.#{ext}")
|
|
521
|
-
end
|
|
522
|
-
|
|
523
|
-
def dispatch_export(path, format)
|
|
524
|
-
if format == 'json'
|
|
525
|
-
export_json(path)
|
|
526
|
-
elsif format == 'html'
|
|
527
|
-
export_html(path)
|
|
528
|
-
else
|
|
529
|
-
export_markdown(path)
|
|
530
|
-
end
|
|
531
|
-
end
|
|
532
|
-
|
|
533
353
|
# rubocop:disable Metrics/AbcSize
|
|
534
354
|
def handle_tools
|
|
535
355
|
lex_gems = Gem::Specification.select { |s| s.name.start_with?('lex-') }
|
|
@@ -549,53 +369,6 @@ module Legion
|
|
|
549
369
|
|
|
550
370
|
# rubocop:enable Metrics/AbcSize
|
|
551
371
|
|
|
552
|
-
def handle_dashboard
|
|
553
|
-
if @app.respond_to?(:toggle_dashboard)
|
|
554
|
-
@app.toggle_dashboard
|
|
555
|
-
else
|
|
556
|
-
@message_stream.add_message(role: :system, content: 'Dashboard not available.')
|
|
557
|
-
end
|
|
558
|
-
:handled
|
|
559
|
-
end
|
|
560
|
-
|
|
561
|
-
def handle_hotkeys
|
|
562
|
-
if @app.respond_to?(:hotkeys)
|
|
563
|
-
bindings = @app.hotkeys.list
|
|
564
|
-
lines = bindings.map { |b| "#{b[:key].inspect} -> #{b[:description]}" }
|
|
565
|
-
text = lines.empty? ? 'No hotkeys registered.' : lines.join("\n")
|
|
566
|
-
@message_stream.add_message(role: :system, content: "Hotkeys:\n#{text}")
|
|
567
|
-
else
|
|
568
|
-
@message_stream.add_message(role: :system, content: 'Hotkeys not available.')
|
|
569
|
-
end
|
|
570
|
-
:handled
|
|
571
|
-
end
|
|
572
|
-
|
|
573
|
-
def handle_system(input)
|
|
574
|
-
text = input.split(nil, 2)[1]
|
|
575
|
-
if text
|
|
576
|
-
if @llm_chat.respond_to?(:with_instructions)
|
|
577
|
-
@llm_chat.with_instructions(text)
|
|
578
|
-
@message_stream.add_message(role: :system, content: 'System prompt updated.')
|
|
579
|
-
else
|
|
580
|
-
@message_stream.add_message(role: :system, content: 'No active LLM session.')
|
|
581
|
-
end
|
|
582
|
-
else
|
|
583
|
-
@message_stream.add_message(role: :system, content: 'Usage: /system <prompt text>')
|
|
584
|
-
end
|
|
585
|
-
:handled
|
|
586
|
-
end
|
|
587
|
-
|
|
588
|
-
def handle_delete(input)
|
|
589
|
-
name = input.split(nil, 2)[1]
|
|
590
|
-
unless name
|
|
591
|
-
@message_stream.add_message(role: :system, content: 'Usage: /delete <session-name>')
|
|
592
|
-
return :handled
|
|
593
|
-
end
|
|
594
|
-
@session_store.delete(name)
|
|
595
|
-
@message_stream.add_message(role: :system, content: "Session '#{name}' deleted.")
|
|
596
|
-
:handled
|
|
597
|
-
end
|
|
598
|
-
|
|
599
372
|
def handle_plan
|
|
600
373
|
@plan_mode = !@plan_mode
|
|
601
374
|
if @plan_mode
|
|
@@ -609,45 +382,12 @@ module Legion
|
|
|
609
382
|
:handled
|
|
610
383
|
end
|
|
611
384
|
|
|
612
|
-
def
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
if selection.start_with?('/')
|
|
619
|
-
handle_slash_command(selection)
|
|
620
|
-
else
|
|
621
|
-
dispatch_screen_by_name(selection)
|
|
622
|
-
end
|
|
623
|
-
:handled
|
|
624
|
-
end
|
|
625
|
-
|
|
626
|
-
def dispatch_screen_by_name(name)
|
|
627
|
-
case name
|
|
628
|
-
when 'dashboard' then handle_dashboard
|
|
629
|
-
when 'extensions' then handle_extensions_screen
|
|
630
|
-
when 'config' then handle_config_screen
|
|
385
|
+
def handle_session(input)
|
|
386
|
+
name = input.split(nil, 2)[1]
|
|
387
|
+
if name
|
|
388
|
+
@session_name = name
|
|
389
|
+
@status_bar.update(session: name)
|
|
631
390
|
end
|
|
632
|
-
end
|
|
633
|
-
|
|
634
|
-
def handle_extensions_screen
|
|
635
|
-
require_relative '../screens/extensions'
|
|
636
|
-
screen = Screens::Extensions.new(@app, output: @output)
|
|
637
|
-
@app.screen_manager.push(screen)
|
|
638
|
-
:handled
|
|
639
|
-
rescue LoadError
|
|
640
|
-
@message_stream.add_message(role: :system, content: 'Extensions screen not available.')
|
|
641
|
-
:handled
|
|
642
|
-
end
|
|
643
|
-
|
|
644
|
-
def handle_config_screen
|
|
645
|
-
require_relative '../screens/config'
|
|
646
|
-
screen = Screens::Config.new(@app, output: @output)
|
|
647
|
-
@app.screen_manager.push(screen)
|
|
648
|
-
:handled
|
|
649
|
-
rescue LoadError
|
|
650
|
-
@message_stream.add_message(role: :system, content: 'Config screen not available.')
|
|
651
391
|
:handled
|
|
652
392
|
end
|
|
653
393
|
|
|
@@ -669,434 +409,6 @@ module Legion
|
|
|
669
409
|
:handled
|
|
670
410
|
end
|
|
671
411
|
|
|
672
|
-
def handle_search(input)
|
|
673
|
-
query = input.split(nil, 2)[1]
|
|
674
|
-
unless query
|
|
675
|
-
@message_stream.add_message(role: :system, content: 'Usage: /search <text>')
|
|
676
|
-
return :handled
|
|
677
|
-
end
|
|
678
|
-
|
|
679
|
-
results = search_messages(query)
|
|
680
|
-
if results.empty?
|
|
681
|
-
@message_stream.add_message(role: :system, content: "No messages matching '#{query}'.")
|
|
682
|
-
else
|
|
683
|
-
lines = results.map { |r| " [#{r[:role]}] #{truncate_text(r[:content], 80)}" }
|
|
684
|
-
@message_stream.add_message(
|
|
685
|
-
role: :system,
|
|
686
|
-
content: "Found #{results.size} message(s) matching '#{query}':\n#{lines.join("\n")}"
|
|
687
|
-
)
|
|
688
|
-
end
|
|
689
|
-
:handled
|
|
690
|
-
end
|
|
691
|
-
|
|
692
|
-
def handle_stats
|
|
693
|
-
@message_stream.add_message(role: :system, content: build_stats_lines.join("\n"))
|
|
694
|
-
:handled
|
|
695
|
-
end
|
|
696
|
-
|
|
697
|
-
def build_stats_lines
|
|
698
|
-
msgs = @message_stream.messages
|
|
699
|
-
counts = count_by_role(msgs)
|
|
700
|
-
total_chars = msgs.sum { |m| m[:content].to_s.length }
|
|
701
|
-
lines = stats_header_lines(msgs, counts, total_chars)
|
|
702
|
-
lines << " Tool calls: #{counts[:tool]}" if counts[:tool].positive?
|
|
703
|
-
lines
|
|
704
|
-
end
|
|
705
|
-
|
|
706
|
-
def count_by_role(msgs)
|
|
707
|
-
%i[user assistant system tool].to_h { |role| [role, msgs.count { |m| m[:role] == role }] }
|
|
708
|
-
end
|
|
709
|
-
|
|
710
|
-
def stats_header_lines(msgs, counts, total_chars)
|
|
711
|
-
[
|
|
712
|
-
"Messages: #{msgs.size} total",
|
|
713
|
-
" User: #{counts[:user]}, Assistant: #{counts[:assistant]}, System: #{counts[:system]}",
|
|
714
|
-
"Characters: #{format_stat_number(total_chars)}",
|
|
715
|
-
"Session: #{@session_name}",
|
|
716
|
-
"Tokens: #{@token_tracker.summary}"
|
|
717
|
-
]
|
|
718
|
-
end
|
|
719
|
-
|
|
720
|
-
def format_stat_number(num)
|
|
721
|
-
num.to_s.chars.reverse.each_slice(3).map(&:join).join(',').reverse
|
|
722
|
-
end
|
|
723
|
-
|
|
724
|
-
def handle_personality(input)
|
|
725
|
-
name = input.split(nil, 2)[1]
|
|
726
|
-
if name && PERSONALITIES.key?(name)
|
|
727
|
-
apply_personality(name)
|
|
728
|
-
elsif name
|
|
729
|
-
available = PERSONALITIES.keys.join(', ')
|
|
730
|
-
@message_stream.add_message(role: :system,
|
|
731
|
-
content: "Unknown personality '#{name}'. Available: #{available}")
|
|
732
|
-
else
|
|
733
|
-
current = @personality || 'default'
|
|
734
|
-
available = PERSONALITIES.keys.join(', ')
|
|
735
|
-
@message_stream.add_message(role: :system, content: "Current: #{current}\nAvailable: #{available}")
|
|
736
|
-
end
|
|
737
|
-
:handled
|
|
738
|
-
end
|
|
739
|
-
|
|
740
|
-
def apply_personality(name)
|
|
741
|
-
@personality = name
|
|
742
|
-
if @llm_chat.respond_to?(:with_instructions)
|
|
743
|
-
@llm_chat.with_instructions(PERSONALITIES[name])
|
|
744
|
-
@message_stream.add_message(role: :system, content: "Personality switched to: #{name}")
|
|
745
|
-
else
|
|
746
|
-
@message_stream.add_message(role: :system, content: "Personality set to: #{name} (no active LLM)")
|
|
747
|
-
end
|
|
748
|
-
end
|
|
749
|
-
|
|
750
|
-
# rubocop:disable Metrics/AbcSize
|
|
751
|
-
def handle_compact(input)
|
|
752
|
-
keep = (input.split(nil, 2)[1] || '5').to_i.clamp(1, 50)
|
|
753
|
-
msgs = @message_stream.messages
|
|
754
|
-
if msgs.size <= keep * 2
|
|
755
|
-
@message_stream.add_message(role: :system, content: 'Conversation is already compact.')
|
|
756
|
-
return :handled
|
|
757
|
-
end
|
|
758
|
-
|
|
759
|
-
system_msgs = msgs.select { |m| m[:role] == :system }
|
|
760
|
-
recent = msgs.reject { |m| m[:role] == :system }.last(keep * 2)
|
|
761
|
-
removed_count = msgs.size - system_msgs.size - recent.size
|
|
762
|
-
@message_stream.messages.replace(system_msgs + recent)
|
|
763
|
-
@message_stream.add_message(
|
|
764
|
-
role: :system,
|
|
765
|
-
content: "Compacted: removed #{removed_count} older messages, kept #{recent.size} recent."
|
|
766
|
-
)
|
|
767
|
-
:handled
|
|
768
|
-
end
|
|
769
|
-
# rubocop:enable Metrics/AbcSize
|
|
770
|
-
|
|
771
|
-
def handle_copy(_input)
|
|
772
|
-
last_assistant = @message_stream.messages.reverse.find { |m| m[:role] == :assistant }
|
|
773
|
-
unless last_assistant
|
|
774
|
-
@message_stream.add_message(role: :system, content: 'No assistant message to copy.')
|
|
775
|
-
return :handled
|
|
776
|
-
end
|
|
777
|
-
|
|
778
|
-
content = last_assistant[:content].to_s
|
|
779
|
-
copy_to_clipboard(content)
|
|
780
|
-
@message_stream.add_message(
|
|
781
|
-
role: :system,
|
|
782
|
-
content: "Copied #{content.length} characters to clipboard."
|
|
783
|
-
)
|
|
784
|
-
:handled
|
|
785
|
-
end
|
|
786
|
-
|
|
787
|
-
def copy_to_clipboard(text)
|
|
788
|
-
IO.popen('pbcopy', 'w') { |io| io.write(text) }
|
|
789
|
-
rescue Errno::ENOENT
|
|
790
|
-
begin
|
|
791
|
-
IO.popen('xclip -selection clipboard', 'w') { |io| io.write(text) }
|
|
792
|
-
rescue Errno::ENOENT
|
|
793
|
-
nil
|
|
794
|
-
end
|
|
795
|
-
end
|
|
796
|
-
|
|
797
|
-
def handle_diff(_input)
|
|
798
|
-
if @loaded_message_count.nil?
|
|
799
|
-
@message_stream.add_message(role: :system, content: 'No session was loaded. Nothing to diff against.')
|
|
800
|
-
return :handled
|
|
801
|
-
end
|
|
802
|
-
|
|
803
|
-
new_count = @message_stream.messages.size - @loaded_message_count
|
|
804
|
-
if new_count <= 0
|
|
805
|
-
@message_stream.add_message(role: :system, content: 'No new messages since session was loaded.')
|
|
806
|
-
else
|
|
807
|
-
new_msgs = @message_stream.messages.last(new_count)
|
|
808
|
-
lines = new_msgs.map { |m| " + [#{m[:role]}] #{truncate_text(m[:content].to_s, 60)}" }
|
|
809
|
-
@message_stream.add_message(
|
|
810
|
-
role: :system,
|
|
811
|
-
content: "#{new_count} new message(s) since load:\n#{lines.join("\n")}"
|
|
812
|
-
)
|
|
813
|
-
end
|
|
814
|
-
:handled
|
|
815
|
-
end
|
|
816
|
-
|
|
817
|
-
def search_messages(query)
|
|
818
|
-
pattern = query.downcase
|
|
819
|
-
@message_stream.messages.select do |msg|
|
|
820
|
-
msg[:content].is_a?(::String) && msg[:content].downcase.include?(pattern)
|
|
821
|
-
end
|
|
822
|
-
end
|
|
823
|
-
|
|
824
|
-
def truncate_text(text, max_length)
|
|
825
|
-
return text if text.length <= max_length
|
|
826
|
-
|
|
827
|
-
"#{text[0...max_length]}..."
|
|
828
|
-
end
|
|
829
|
-
|
|
830
|
-
def detect_provider
|
|
831
|
-
cfg = safe_config
|
|
832
|
-
provider = cfg[:provider].to_s.downcase
|
|
833
|
-
return provider if Components::TokenTracker::PROVIDER_PRICING.key?(provider)
|
|
834
|
-
|
|
835
|
-
'claude'
|
|
836
|
-
end
|
|
837
|
-
|
|
838
|
-
def track_response_tokens(response)
|
|
839
|
-
return unless response.respond_to?(:input_tokens)
|
|
840
|
-
|
|
841
|
-
model_id = response.respond_to?(:model) ? response.model.to_s : nil
|
|
842
|
-
@token_tracker.track(
|
|
843
|
-
input_tokens: response.input_tokens.to_i,
|
|
844
|
-
output_tokens: response.output_tokens.to_i,
|
|
845
|
-
model: model_id
|
|
846
|
-
)
|
|
847
|
-
@status_bar.update(
|
|
848
|
-
tokens: @token_tracker.total_input_tokens + @token_tracker.total_output_tokens,
|
|
849
|
-
cost: @token_tracker.total_cost
|
|
850
|
-
)
|
|
851
|
-
end
|
|
852
|
-
|
|
853
|
-
def export_markdown(path)
|
|
854
|
-
lines = ["# Chat Export\n", "_Exported: #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}_\n\n---\n"]
|
|
855
|
-
@message_stream.messages.each do |msg|
|
|
856
|
-
role_label = msg[:role].to_s.capitalize
|
|
857
|
-
lines << "\n**#{role_label}**\n\n#{msg[:content]}\n"
|
|
858
|
-
end
|
|
859
|
-
File.write(path, lines.join)
|
|
860
|
-
end
|
|
861
|
-
|
|
862
|
-
def export_json(path)
|
|
863
|
-
require 'json'
|
|
864
|
-
data = {
|
|
865
|
-
exported_at: Time.now.iso8601,
|
|
866
|
-
token_summary: @token_tracker.summary,
|
|
867
|
-
messages: @message_stream.messages.map { |m| { role: m[:role].to_s, content: m[:content] } }
|
|
868
|
-
}
|
|
869
|
-
File.write(path, ::JSON.pretty_generate(data))
|
|
870
|
-
end
|
|
871
|
-
|
|
872
|
-
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
|
873
|
-
def export_html(path)
|
|
874
|
-
lines = [
|
|
875
|
-
'<!DOCTYPE html><html><head>',
|
|
876
|
-
'<meta charset="utf-8">',
|
|
877
|
-
'<title>Chat Export</title>',
|
|
878
|
-
'<style>',
|
|
879
|
-
'body { font-family: system-ui; max-width: 800px; margin: 0 auto; ' \
|
|
880
|
-
'padding: 20px; background: #1e1b2e; color: #d0cce6; }',
|
|
881
|
-
'.msg { margin: 12px 0; padding: 8px 12px; border-radius: 8px; }',
|
|
882
|
-
'.user { background: #2a2640; }',
|
|
883
|
-
'.assistant { background: #1a1730; }',
|
|
884
|
-
'.system { background: #25223a; color: #8b85a8; font-style: italic; }',
|
|
885
|
-
'.role { font-weight: bold; color: #9d91e6; font-size: 0.85em; }',
|
|
886
|
-
'</style></head><body>',
|
|
887
|
-
'<h1>Chat Export</h1>',
|
|
888
|
-
"<p>Exported: #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}</p>"
|
|
889
|
-
]
|
|
890
|
-
@message_stream.messages.each do |msg|
|
|
891
|
-
role = msg[:role].to_s
|
|
892
|
-
content = escape_html(msg[:content].to_s).gsub("\n", '<br>')
|
|
893
|
-
lines << "<div class='msg #{role}'>"
|
|
894
|
-
lines << "<span class='role'>#{role.capitalize}</span>"
|
|
895
|
-
lines << "<p>#{content}</p>"
|
|
896
|
-
lines << '</div>'
|
|
897
|
-
end
|
|
898
|
-
lines << '</body></html>'
|
|
899
|
-
File.write(path, lines.join("\n"))
|
|
900
|
-
end
|
|
901
|
-
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize
|
|
902
|
-
|
|
903
|
-
def escape_html(text)
|
|
904
|
-
text.gsub('&', '&').gsub('<', '<').gsub('>', '>').gsub('"', '"')
|
|
905
|
-
end
|
|
906
|
-
|
|
907
|
-
def handle_undo
|
|
908
|
-
msgs = @message_stream.messages
|
|
909
|
-
last_user_idx = msgs.rindex { |m| m[:role] == :user }
|
|
910
|
-
unless last_user_idx
|
|
911
|
-
@message_stream.add_message(role: :system, content: 'Nothing to undo.')
|
|
912
|
-
return :handled
|
|
913
|
-
end
|
|
914
|
-
|
|
915
|
-
msgs.slice!(last_user_idx..)
|
|
916
|
-
:handled
|
|
917
|
-
end
|
|
918
|
-
|
|
919
|
-
def handle_history
|
|
920
|
-
entries = @input_bar.history
|
|
921
|
-
if entries.empty?
|
|
922
|
-
@message_stream.add_message(role: :system, content: 'No input history.')
|
|
923
|
-
else
|
|
924
|
-
recent = entries.last(20)
|
|
925
|
-
lines = recent.each_with_index.map { |entry, i| " #{i + 1}. #{entry}" }
|
|
926
|
-
@message_stream.add_message(role: :system,
|
|
927
|
-
content: "Input history (last #{recent.size}):\n#{lines.join("\n")}")
|
|
928
|
-
end
|
|
929
|
-
:handled
|
|
930
|
-
end
|
|
931
|
-
|
|
932
|
-
def handle_pin(input)
|
|
933
|
-
idx_str = input.split(nil, 2)[1]
|
|
934
|
-
msg = if idx_str
|
|
935
|
-
@message_stream.messages[idx_str.to_i]
|
|
936
|
-
else
|
|
937
|
-
@message_stream.messages.reverse.find { |m| m[:role] == :assistant }
|
|
938
|
-
end
|
|
939
|
-
unless msg
|
|
940
|
-
@message_stream.add_message(role: :system, content: 'No message to pin.')
|
|
941
|
-
return :handled
|
|
942
|
-
end
|
|
943
|
-
|
|
944
|
-
@pinned_messages << msg
|
|
945
|
-
preview = truncate_text(msg[:content].to_s, 60)
|
|
946
|
-
@message_stream.add_message(role: :system, content: "Pinned: #{preview}")
|
|
947
|
-
:handled
|
|
948
|
-
end
|
|
949
|
-
|
|
950
|
-
def handle_pins
|
|
951
|
-
if @pinned_messages.empty?
|
|
952
|
-
@message_stream.add_message(role: :system, content: 'No pinned messages.')
|
|
953
|
-
else
|
|
954
|
-
lines = @pinned_messages.each_with_index.map do |msg, i|
|
|
955
|
-
" #{i + 1}. [#{msg[:role]}] #{truncate_text(msg[:content].to_s, 70)}"
|
|
956
|
-
end
|
|
957
|
-
@message_stream.add_message(role: :system,
|
|
958
|
-
content: "Pinned messages (#{@pinned_messages.size}):\n#{lines.join("\n")}")
|
|
959
|
-
end
|
|
960
|
-
:handled
|
|
961
|
-
end
|
|
962
|
-
|
|
963
|
-
def handle_rename(input)
|
|
964
|
-
name = input.split(nil, 2)[1]
|
|
965
|
-
unless name
|
|
966
|
-
@message_stream.add_message(role: :system, content: 'Usage: /rename <new-name>')
|
|
967
|
-
return :handled
|
|
968
|
-
end
|
|
969
|
-
|
|
970
|
-
old_name = @session_name
|
|
971
|
-
@session_store.delete(old_name) if old_name != 'default'
|
|
972
|
-
@session_name = name
|
|
973
|
-
@status_bar.update(session: name)
|
|
974
|
-
@session_store.save(name, messages: @message_stream.messages)
|
|
975
|
-
@message_stream.add_message(role: :system, content: "Session renamed to '#{name}'.")
|
|
976
|
-
:handled
|
|
977
|
-
end
|
|
978
|
-
|
|
979
|
-
# rubocop:disable Metrics/AbcSize
|
|
980
|
-
def handle_context
|
|
981
|
-
cfg = safe_config
|
|
982
|
-
model_info = @llm_chat.respond_to?(:model) ? @llm_chat.model.to_s : (cfg[:provider] || 'none')
|
|
983
|
-
sys_prompt = if @llm_chat.respond_to?(:instructions) && @llm_chat.instructions
|
|
984
|
-
truncate_text(@llm_chat.instructions.to_s, 80)
|
|
985
|
-
else
|
|
986
|
-
'default'
|
|
987
|
-
end
|
|
988
|
-
lines = [
|
|
989
|
-
'Session Context:',
|
|
990
|
-
" Model/Provider : #{model_info}",
|
|
991
|
-
" Personality : #{@personality || 'default'}",
|
|
992
|
-
" Plan mode : #{@plan_mode ? 'on' : 'off'}",
|
|
993
|
-
" System prompt : #{sys_prompt}",
|
|
994
|
-
" Session : #{@session_name}",
|
|
995
|
-
" Messages : #{@message_stream.messages.size}",
|
|
996
|
-
" Pinned : #{@pinned_messages.size}",
|
|
997
|
-
" Tokens : #{@token_tracker.summary}"
|
|
998
|
-
]
|
|
999
|
-
@message_stream.add_message(role: :system, content: lines.join("\n"))
|
|
1000
|
-
:handled
|
|
1001
|
-
end
|
|
1002
|
-
# rubocop:enable Metrics/AbcSize
|
|
1003
|
-
|
|
1004
|
-
def handle_alias(input)
|
|
1005
|
-
parts = input.split(nil, 3)
|
|
1006
|
-
if parts.size < 2
|
|
1007
|
-
if @aliases.empty?
|
|
1008
|
-
@message_stream.add_message(role: :system, content: 'No aliases defined.')
|
|
1009
|
-
else
|
|
1010
|
-
lines = @aliases.map { |k, v| " #{k} => #{v}" }
|
|
1011
|
-
@message_stream.add_message(role: :system, content: "Aliases:\n#{lines.join("\n")}")
|
|
1012
|
-
end
|
|
1013
|
-
return :handled
|
|
1014
|
-
end
|
|
1015
|
-
|
|
1016
|
-
shortname = parts[1]
|
|
1017
|
-
expansion = parts[2]
|
|
1018
|
-
unless expansion
|
|
1019
|
-
@message_stream.add_message(role: :system, content: 'Usage: /alias <shortname> <command and args>')
|
|
1020
|
-
return :handled
|
|
1021
|
-
end
|
|
1022
|
-
|
|
1023
|
-
alias_key = shortname.start_with?('/') ? shortname : "/#{shortname}"
|
|
1024
|
-
@aliases[alias_key] = expansion
|
|
1025
|
-
@message_stream.add_message(role: :system, content: "Alias created: #{alias_key} => #{expansion}")
|
|
1026
|
-
:handled
|
|
1027
|
-
end
|
|
1028
|
-
|
|
1029
|
-
def handle_snippet(input)
|
|
1030
|
-
parts = input.split(nil, 3)
|
|
1031
|
-
subcommand = parts[1]
|
|
1032
|
-
name = parts[2]
|
|
1033
|
-
|
|
1034
|
-
case subcommand
|
|
1035
|
-
when 'save'
|
|
1036
|
-
snippet_save(name)
|
|
1037
|
-
when 'load'
|
|
1038
|
-
snippet_load(name)
|
|
1039
|
-
when 'list'
|
|
1040
|
-
snippet_list
|
|
1041
|
-
when 'delete'
|
|
1042
|
-
snippet_delete(name)
|
|
1043
|
-
else
|
|
1044
|
-
@message_stream.add_message(
|
|
1045
|
-
role: :system,
|
|
1046
|
-
content: 'Usage: /snippet save|load|list|delete <name>'
|
|
1047
|
-
)
|
|
1048
|
-
end
|
|
1049
|
-
:handled
|
|
1050
|
-
end
|
|
1051
|
-
|
|
1052
|
-
def handle_debug
|
|
1053
|
-
@debug_mode = !@debug_mode
|
|
1054
|
-
if @debug_mode
|
|
1055
|
-
@status_bar.update(debug_mode: true)
|
|
1056
|
-
@message_stream.add_message(role: :system, content: 'Debug mode ON -- internal state shown below.')
|
|
1057
|
-
else
|
|
1058
|
-
@status_bar.update(debug_mode: false)
|
|
1059
|
-
@message_stream.add_message(role: :system, content: 'Debug mode OFF.')
|
|
1060
|
-
end
|
|
1061
|
-
:handled
|
|
1062
|
-
end
|
|
1063
|
-
|
|
1064
|
-
def handle_uptime
|
|
1065
|
-
elapsed = Time.now - @session_start
|
|
1066
|
-
hours = (elapsed / 3600).to_i
|
|
1067
|
-
minutes = ((elapsed % 3600) / 60).to_i
|
|
1068
|
-
seconds = (elapsed % 60).to_i
|
|
1069
|
-
@message_stream.add_message(role: :system, content: "Session uptime: #{hours}h #{minutes}m #{seconds}s")
|
|
1070
|
-
:handled
|
|
1071
|
-
end
|
|
1072
|
-
|
|
1073
|
-
# rubocop:disable Metrics/AbcSize
|
|
1074
|
-
def handle_bookmark
|
|
1075
|
-
require 'fileutils'
|
|
1076
|
-
if @pinned_messages.empty?
|
|
1077
|
-
@message_stream.add_message(role: :system, content: 'No pinned messages to export.')
|
|
1078
|
-
return :handled
|
|
1079
|
-
end
|
|
1080
|
-
|
|
1081
|
-
exports_dir = File.expand_path('~/.legionio/exports')
|
|
1082
|
-
FileUtils.mkdir_p(exports_dir)
|
|
1083
|
-
timestamp = Time.now.strftime('%Y%m%d-%H%M%S')
|
|
1084
|
-
path = File.join(exports_dir, "bookmarks-#{timestamp}.md")
|
|
1085
|
-
lines = ["# Pinned Messages\n", "_Exported: #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}_\n\n---\n"]
|
|
1086
|
-
@pinned_messages.each_with_index do |msg, i|
|
|
1087
|
-
role_label = msg[:role].to_s.capitalize
|
|
1088
|
-
lines << "\n## Bookmark #{i + 1} (#{role_label})\n\n#{msg[:content]}\n"
|
|
1089
|
-
end
|
|
1090
|
-
File.write(path, lines.join)
|
|
1091
|
-
@message_stream.add_message(role: :system, content: "Bookmarks exported to: #{path}")
|
|
1092
|
-
:handled
|
|
1093
|
-
rescue StandardError => e
|
|
1094
|
-
@message_stream.add_message(role: :system, content: "Bookmark export failed: #{e.message}")
|
|
1095
|
-
:handled
|
|
1096
|
-
end
|
|
1097
|
-
|
|
1098
|
-
# rubocop:enable Metrics/AbcSize
|
|
1099
|
-
|
|
1100
412
|
def debug_segment
|
|
1101
413
|
return nil unless @debug_mode
|
|
1102
414
|
|
|
@@ -1109,91 +421,6 @@ module Legion
|
|
|
1109
421
|
"pinned:#{@pinned_messages.size}"
|
|
1110
422
|
end
|
|
1111
423
|
|
|
1112
|
-
def snippet_dir
|
|
1113
|
-
File.expand_path('~/.legionio/snippets')
|
|
1114
|
-
end
|
|
1115
|
-
|
|
1116
|
-
# rubocop:disable Metrics/AbcSize
|
|
1117
|
-
def snippet_save(name)
|
|
1118
|
-
unless name
|
|
1119
|
-
@message_stream.add_message(role: :system, content: 'Usage: /snippet save <name>')
|
|
1120
|
-
return
|
|
1121
|
-
end
|
|
1122
|
-
|
|
1123
|
-
last_assistant = @message_stream.messages.reverse.find { |m| m[:role] == :assistant }
|
|
1124
|
-
unless last_assistant
|
|
1125
|
-
@message_stream.add_message(role: :system, content: 'No assistant message to save as snippet.')
|
|
1126
|
-
return
|
|
1127
|
-
end
|
|
1128
|
-
|
|
1129
|
-
require 'fileutils'
|
|
1130
|
-
FileUtils.mkdir_p(snippet_dir)
|
|
1131
|
-
path = File.join(snippet_dir, "#{name}.txt")
|
|
1132
|
-
File.write(path, last_assistant[:content].to_s)
|
|
1133
|
-
@snippets[name] = last_assistant[:content].to_s
|
|
1134
|
-
@message_stream.add_message(role: :system, content: "Snippet '#{name}' saved.")
|
|
1135
|
-
end
|
|
1136
|
-
# rubocop:enable Metrics/AbcSize
|
|
1137
|
-
|
|
1138
|
-
def snippet_load(name)
|
|
1139
|
-
unless name
|
|
1140
|
-
@message_stream.add_message(role: :system, content: 'Usage: /snippet load <name>')
|
|
1141
|
-
return
|
|
1142
|
-
end
|
|
1143
|
-
|
|
1144
|
-
content = @snippets[name]
|
|
1145
|
-
if content.nil?
|
|
1146
|
-
path = File.join(snippet_dir, "#{name}.txt")
|
|
1147
|
-
content = File.read(path) if File.exist?(path)
|
|
1148
|
-
end
|
|
1149
|
-
|
|
1150
|
-
unless content
|
|
1151
|
-
@message_stream.add_message(role: :system, content: "Snippet '#{name}' not found.")
|
|
1152
|
-
return
|
|
1153
|
-
end
|
|
1154
|
-
|
|
1155
|
-
@snippets[name] = content
|
|
1156
|
-
@message_stream.add_message(role: :user, content: content)
|
|
1157
|
-
@message_stream.add_message(role: :system, content: "Snippet '#{name}' inserted.")
|
|
1158
|
-
end
|
|
1159
|
-
|
|
1160
|
-
# rubocop:disable Metrics/AbcSize
|
|
1161
|
-
def snippet_list
|
|
1162
|
-
disk_snippets = Dir.glob(File.join(snippet_dir, '*.txt')).map { |f| File.basename(f, '.txt') }
|
|
1163
|
-
all_names = (@snippets.keys + disk_snippets).uniq.sort
|
|
1164
|
-
|
|
1165
|
-
if all_names.empty?
|
|
1166
|
-
@message_stream.add_message(role: :system, content: 'No snippets saved.')
|
|
1167
|
-
return
|
|
1168
|
-
end
|
|
1169
|
-
|
|
1170
|
-
lines = all_names.map do |sname|
|
|
1171
|
-
content = @snippets[sname] || begin
|
|
1172
|
-
path = File.join(snippet_dir, "#{sname}.txt")
|
|
1173
|
-
File.exist?(path) ? File.read(path) : ''
|
|
1174
|
-
end
|
|
1175
|
-
" #{sname}: #{truncate_text(content.to_s, 60)}"
|
|
1176
|
-
end
|
|
1177
|
-
@message_stream.add_message(role: :system, content: "Snippets (#{all_names.size}):\n#{lines.join("\n")}")
|
|
1178
|
-
end
|
|
1179
|
-
# rubocop:enable Metrics/AbcSize
|
|
1180
|
-
|
|
1181
|
-
def snippet_delete(name)
|
|
1182
|
-
unless name
|
|
1183
|
-
@message_stream.add_message(role: :system, content: 'Usage: /snippet delete <name>')
|
|
1184
|
-
return
|
|
1185
|
-
end
|
|
1186
|
-
|
|
1187
|
-
@snippets.delete(name)
|
|
1188
|
-
path = File.join(snippet_dir, "#{name}.txt")
|
|
1189
|
-
if File.exist?(path)
|
|
1190
|
-
File.delete(path)
|
|
1191
|
-
@message_stream.add_message(role: :system, content: "Snippet '#{name}' deleted.")
|
|
1192
|
-
else
|
|
1193
|
-
@message_stream.add_message(role: :system, content: "Snippet '#{name}' not found.")
|
|
1194
|
-
end
|
|
1195
|
-
end
|
|
1196
|
-
|
|
1197
424
|
def build_default_input_bar
|
|
1198
425
|
cfg = safe_config
|
|
1199
426
|
name = cfg[:name] || 'User'
|
|
@@ -1213,6 +440,29 @@ module Legion
|
|
|
1213
440
|
rescue StandardError
|
|
1214
441
|
24
|
|
1215
442
|
end
|
|
443
|
+
|
|
444
|
+
def detect_provider
|
|
445
|
+
cfg = safe_config
|
|
446
|
+
provider = cfg[:provider].to_s.downcase
|
|
447
|
+
return provider if Components::TokenTracker::PROVIDER_PRICING.key?(provider)
|
|
448
|
+
|
|
449
|
+
'claude'
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
def track_response_tokens(response)
|
|
453
|
+
return unless response.respond_to?(:input_tokens)
|
|
454
|
+
|
|
455
|
+
model_id = response.respond_to?(:model) ? response.model.to_s : nil
|
|
456
|
+
@token_tracker.track(
|
|
457
|
+
input_tokens: response.input_tokens.to_i,
|
|
458
|
+
output_tokens: response.output_tokens.to_i,
|
|
459
|
+
model: model_id
|
|
460
|
+
)
|
|
461
|
+
@status_bar.update(
|
|
462
|
+
tokens: @token_tracker.total_input_tokens + @token_tracker.total_output_tokens,
|
|
463
|
+
cost: @token_tracker.total_cost
|
|
464
|
+
)
|
|
465
|
+
end
|
|
1216
466
|
end
|
|
1217
467
|
# rubocop:enable Metrics/ClassLength
|
|
1218
468
|
end
|