legion-tty 0.4.18 → 0.4.20
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 +16 -0
- data/README.md +25 -6
- data/lib/legion/tty/components/message_stream.rb +19 -2
- data/lib/legion/tty/screens/chat/custom_commands.rb +133 -0
- data/lib/legion/tty/screens/chat/message_commands.rb +35 -0
- data/lib/legion/tty/screens/chat/session_commands.rb +76 -0
- data/lib/legion/tty/screens/chat/ui_commands.rb +111 -0
- data/lib/legion/tty/screens/chat.rb +12 -1
- data/lib/legion/tty/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f2d9ef0fe2336f2750785f4742bc14661032836b05fab83062ba5de23c60cbbc
|
|
4
|
+
data.tar.gz: cf9fe410cc87ba108cc52e994b5210351bb403b84aa43a27cbcb5ad94ff65232
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 194101f68a10f6e0c9b7a9c14d38690adcfe37724fcb5c2ee894d5a13b757a897aa3b4e85fe48e5db5b44b14f597ffffa9ce1a88ede2da54a1858ca42f759bc9
|
|
7
|
+
data.tar.gz: 1eb5e7cafbdc7e1025fcdd8424256eea456c3b24f3d03077effd4417a694bddf5f77fd232b6514e89dad53d813161b2d753b6e9a4d7564c1b13ecea6dde1cd95
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.4.20] - 2026-03-19
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- `/prompt save|load|list|delete` command: persist and reuse custom system prompts
|
|
7
|
+
- `/reset` command: reset session to clean state (clears messages, modes, aliases, macros)
|
|
8
|
+
- `/replace old >>> new` command: find and replace text across all messages
|
|
9
|
+
- `/highlight` command: highlight text patterns in message rendering with ANSI color
|
|
10
|
+
|
|
11
|
+
## [0.4.19] - 2026-03-19
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- `/chain` command: send a sequence of pipe-separated prompts to the LLM sequentially
|
|
15
|
+
- `/info` command: comprehensive session info (modes, counts, aliases, snippets, macros, provider)
|
|
16
|
+
- `/scroll [top|bottom|N]` command: navigate to specific scroll position in message stream
|
|
17
|
+
- `/summary` command: generate a local conversation summary (topics, lengths, duration)
|
|
18
|
+
|
|
3
19
|
## [0.4.18] - 2026-03-19
|
|
4
20
|
|
|
5
21
|
### Added
|
data/README.md
CHANGED
|
@@ -2,15 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
Rich terminal UI for the LegionIO async cognition engine.
|
|
4
4
|
|
|
5
|
-
**Version**: 0.4.
|
|
5
|
+
**Version**: 0.4.18
|
|
6
6
|
|
|
7
|
-
Think Claude Code meets Codex CLI, but for LegionIO: onboarding wizard with identity detection, streaming AI chat shell with
|
|
7
|
+
Think Claude Code meets Codex CLI, but for LegionIO: onboarding wizard with identity detection, streaming AI chat shell with 60 slash commands, operational dashboard, extensions browser, config editor, and session persistence - all rendered with the [tty-ruby](https://ttytoolkit.org/) gem ecosystem.
|
|
8
8
|
|
|
9
9
|
## Features
|
|
10
10
|
|
|
11
11
|
- **Onboarding wizard** - First-run setup with Kerberos identity detection, GitHub profile probing, environment scanning, and LLM provider selection
|
|
12
12
|
- **Digital rain intro** - Matrix-style rain using discovered LEX extension names
|
|
13
|
-
- **AI chat shell** - Streaming LLM chat with
|
|
13
|
+
- **AI chat shell** - Streaming LLM chat with 60 slash commands, tab completion, markdown rendering, and tool panels
|
|
14
14
|
- **Operational dashboard** - Service/LLM status, extension inventory, system info, panel navigation (Ctrl+D or `/dashboard`)
|
|
15
15
|
- **Extensions browser** - Browse installed LEX gems by category with detail view and homepage opener ('o' key)
|
|
16
16
|
- **Config viewer/editor** - View and edit `~/.legionio/settings/*.json` with vault:// masking and JSON validation
|
|
@@ -115,6 +115,25 @@ legion chat prompt "explain async cognition"
|
|
|
115
115
|
| `/uptime` | Show session elapsed time |
|
|
116
116
|
| `/bookmark` | Export pinned messages to file |
|
|
117
117
|
| `/time` | Show current date and time |
|
|
118
|
+
| `/autosave [N\|off]` | Toggle periodic auto-save with interval |
|
|
119
|
+
| `/react <emoji>` | Add emoji reaction to a message |
|
|
120
|
+
| `/macro <action>` | Record/stop/play/list/delete command macros |
|
|
121
|
+
| `/tag <label>` | Tag a message with a label |
|
|
122
|
+
| `/tags [label]` | Show tag statistics or filter by tag |
|
|
123
|
+
| `/repeat` | Re-execute the last slash command |
|
|
124
|
+
| `/count <pattern>` | Count messages matching a pattern |
|
|
125
|
+
| `/template [name]` | List or use prompt templates |
|
|
126
|
+
| `/fav [N]` | Favorite a message (persists to disk) |
|
|
127
|
+
| `/favs` | Show all favorited messages |
|
|
128
|
+
| `/log [N]` | View last N lines of boot log |
|
|
129
|
+
| `/version` | Show version and platform info |
|
|
130
|
+
| `/focus` | Toggle minimal UI (hide status bar) |
|
|
131
|
+
| `/retry` | Resend last message to LLM |
|
|
132
|
+
| `/merge <session>` | Merge another session into current |
|
|
133
|
+
| `/sort [length\|role]` | Show messages sorted by length or role |
|
|
134
|
+
| `/import <path>` | Import session from a JSON file |
|
|
135
|
+
| `/mute` | Toggle system message display |
|
|
136
|
+
| `/wc` | Show word count statistics |
|
|
118
137
|
|
|
119
138
|
## Hotkeys
|
|
120
139
|
|
|
@@ -139,7 +158,7 @@ legion-tty
|
|
|
139
158
|
|
|
140
159
|
Screens/
|
|
141
160
|
Onboarding # First-run wizard (rain -> intro -> wizard -> reveal)
|
|
142
|
-
Chat # AI chat REPL with streaming +
|
|
161
|
+
Chat # AI chat REPL with streaming + 60 slash commands
|
|
143
162
|
SessionCommands # save/load/sessions/delete/rename
|
|
144
163
|
ExportCommands # export/bookmark/html/json/markdown
|
|
145
164
|
MessageCommands # compact/copy/diff/search/grep/undo/pin/pins
|
|
@@ -194,8 +213,8 @@ Boot logs go to `~/.legionio/logs/tty-boot.log`.
|
|
|
194
213
|
|
|
195
214
|
```bash
|
|
196
215
|
bundle install
|
|
197
|
-
bundle exec rspec #
|
|
198
|
-
bundle exec rubocop #
|
|
216
|
+
bundle exec rspec # 1143 examples, 0 failures
|
|
217
|
+
bundle exec rubocop # 106 files, 0 offenses
|
|
199
218
|
```
|
|
200
219
|
|
|
201
220
|
## License
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'English'
|
|
3
4
|
require_relative '../theme'
|
|
4
5
|
|
|
5
6
|
module Legion
|
|
@@ -8,12 +9,16 @@ module Legion
|
|
|
8
9
|
# rubocop:disable Metrics/ClassLength
|
|
9
10
|
class MessageStream
|
|
10
11
|
attr_reader :messages, :scroll_offset
|
|
11
|
-
attr_accessor :mute_system
|
|
12
|
+
attr_accessor :mute_system, :highlights
|
|
13
|
+
|
|
14
|
+
HIGHLIGHT_COLOR = "\e[1;33m"
|
|
15
|
+
HIGHLIGHT_RESET = "\e[0m"
|
|
12
16
|
|
|
13
17
|
def initialize
|
|
14
18
|
@messages = []
|
|
15
19
|
@scroll_offset = 0
|
|
16
20
|
@mute_system = false
|
|
21
|
+
@highlights = []
|
|
17
22
|
end
|
|
18
23
|
|
|
19
24
|
def add_message(role:, content:)
|
|
@@ -96,7 +101,8 @@ module Legion
|
|
|
96
101
|
def user_lines(msg, _width)
|
|
97
102
|
ts = format_timestamp(msg[:timestamp])
|
|
98
103
|
header = "#{Theme.c(:accent, 'You')} #{Theme.c(:muted, ts)}"
|
|
99
|
-
|
|
104
|
+
content = apply_highlights(msg[:content].to_s)
|
|
105
|
+
lines = ['', "#{header}: #{content}"]
|
|
100
106
|
lines << reaction_line(msg) if msg[:reactions]&.any?
|
|
101
107
|
lines
|
|
102
108
|
end
|
|
@@ -109,6 +115,7 @@ module Legion
|
|
|
109
115
|
|
|
110
116
|
def assistant_lines(msg, width)
|
|
111
117
|
rendered = render_markdown(msg[:content], width)
|
|
118
|
+
rendered = apply_highlights(rendered)
|
|
112
119
|
lines = ['', *rendered.split("\n")]
|
|
113
120
|
lines << reaction_line(msg) if msg[:reactions]&.any?
|
|
114
121
|
lines
|
|
@@ -140,6 +147,16 @@ module Legion
|
|
|
140
147
|
msg[:tool_panels].flat_map { |panel| panel.render(width: width).split("\n") }
|
|
141
148
|
end
|
|
142
149
|
|
|
150
|
+
def apply_highlights(text)
|
|
151
|
+
return text if @highlights.nil? || @highlights.empty?
|
|
152
|
+
|
|
153
|
+
@highlights.reduce(text) do |result, pattern|
|
|
154
|
+
result.gsub(pattern) { "#{HIGHLIGHT_COLOR}#{$LAST_MATCH_INFO}#{HIGHLIGHT_RESET}" }
|
|
155
|
+
end
|
|
156
|
+
rescue StandardError
|
|
157
|
+
text
|
|
158
|
+
end
|
|
159
|
+
|
|
143
160
|
def apply_tool_panel_update(panel, status:, duration:, result:, error:)
|
|
144
161
|
panel.instance_variable_set(:@status, status)
|
|
145
162
|
panel.instance_variable_set(:@duration, duration) if duration
|
|
@@ -97,6 +97,103 @@ module Legion
|
|
|
97
97
|
:handled
|
|
98
98
|
end
|
|
99
99
|
|
|
100
|
+
def handle_prompt(input)
|
|
101
|
+
parts = input.split(nil, 3)
|
|
102
|
+
subcommand = parts[1]
|
|
103
|
+
name = parts[2]
|
|
104
|
+
|
|
105
|
+
case subcommand
|
|
106
|
+
when 'save'
|
|
107
|
+
prompt_save(name)
|
|
108
|
+
when 'load'
|
|
109
|
+
prompt_load(name)
|
|
110
|
+
when 'list'
|
|
111
|
+
prompt_list
|
|
112
|
+
when 'delete'
|
|
113
|
+
prompt_delete(name)
|
|
114
|
+
else
|
|
115
|
+
@message_stream.add_message(
|
|
116
|
+
role: :system,
|
|
117
|
+
content: 'Usage: /prompt save|load|list|delete <name>'
|
|
118
|
+
)
|
|
119
|
+
end
|
|
120
|
+
:handled
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def prompt_dir
|
|
124
|
+
File.expand_path('~/.legionio/prompts')
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def prompt_save(name)
|
|
128
|
+
unless name
|
|
129
|
+
@message_stream.add_message(role: :system, content: 'Usage: /prompt save <name>')
|
|
130
|
+
return
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
current = @llm_chat.respond_to?(:instructions) ? @llm_chat.instructions.to_s : ''
|
|
134
|
+
if current.empty?
|
|
135
|
+
@message_stream.add_message(role: :system, content: 'No system prompt is currently set.')
|
|
136
|
+
return
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
require 'fileutils'
|
|
140
|
+
FileUtils.mkdir_p(prompt_dir)
|
|
141
|
+
path = File.join(prompt_dir, "#{name}.txt")
|
|
142
|
+
File.write(path, current)
|
|
143
|
+
@message_stream.add_message(role: :system, content: "Prompt '#{name}' saved.")
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def prompt_load(name)
|
|
147
|
+
unless name
|
|
148
|
+
@message_stream.add_message(role: :system, content: 'Usage: /prompt load <name>')
|
|
149
|
+
return
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
path = File.join(prompt_dir, "#{name}.txt")
|
|
153
|
+
unless File.exist?(path)
|
|
154
|
+
@message_stream.add_message(role: :system, content: "Prompt '#{name}' not found.")
|
|
155
|
+
return
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
content = File.read(path)
|
|
159
|
+
@llm_chat.with_instructions(content) if @llm_chat.respond_to?(:with_instructions)
|
|
160
|
+
@message_stream.add_message(role: :system, content: "Prompt '#{name}' loaded as system prompt.")
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# rubocop:disable Metrics/AbcSize
|
|
164
|
+
def prompt_list
|
|
165
|
+
disk_prompts = Dir.glob(File.join(prompt_dir, '*.txt')).map { |f| File.basename(f, '.txt') }.sort
|
|
166
|
+
|
|
167
|
+
if disk_prompts.empty?
|
|
168
|
+
@message_stream.add_message(role: :system, content: 'No prompts saved.')
|
|
169
|
+
return
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
lines = disk_prompts.map do |pname|
|
|
173
|
+
path = File.join(prompt_dir, "#{pname}.txt")
|
|
174
|
+
preview = File.exist?(path) ? truncate_text(File.read(path), 60) : ''
|
|
175
|
+
" #{pname}: #{preview}"
|
|
176
|
+
end
|
|
177
|
+
@message_stream.add_message(role: :system,
|
|
178
|
+
content: "Prompts (#{disk_prompts.size}):\n#{lines.join("\n")}")
|
|
179
|
+
end
|
|
180
|
+
# rubocop:enable Metrics/AbcSize
|
|
181
|
+
|
|
182
|
+
def prompt_delete(name)
|
|
183
|
+
unless name
|
|
184
|
+
@message_stream.add_message(role: :system, content: 'Usage: /prompt delete <name>')
|
|
185
|
+
return
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
path = File.join(prompt_dir, "#{name}.txt")
|
|
189
|
+
if File.exist?(path)
|
|
190
|
+
File.delete(path)
|
|
191
|
+
@message_stream.add_message(role: :system, content: "Prompt '#{name}' deleted.")
|
|
192
|
+
else
|
|
193
|
+
@message_stream.add_message(role: :system, content: "Prompt '#{name}' not found.")
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
100
197
|
def snippet_dir
|
|
101
198
|
File.expand_path('~/.legionio/snippets')
|
|
102
199
|
end
|
|
@@ -280,6 +377,42 @@ module Legion
|
|
|
280
377
|
@message_stream.add_message(role: :system, content: "Snippet '#{name}' not found.")
|
|
281
378
|
end
|
|
282
379
|
end
|
|
380
|
+
|
|
381
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
382
|
+
def handle_chain(input)
|
|
383
|
+
args = input.split(nil, 2)[1]
|
|
384
|
+
unless args
|
|
385
|
+
@message_stream.add_message(
|
|
386
|
+
role: :system,
|
|
387
|
+
content: 'Usage: /chain prompt1 | prompt2 | prompt3'
|
|
388
|
+
)
|
|
389
|
+
return :handled
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
unless @llm_chat || daemon_available?
|
|
393
|
+
@message_stream.add_message(role: :system, content: 'LLM not configured. Cannot run chain.')
|
|
394
|
+
return :handled
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
prompts = args.split('|').map(&:strip).reject(&:empty?)
|
|
398
|
+
if prompts.empty?
|
|
399
|
+
@message_stream.add_message(role: :system, content: 'Usage: /chain prompt1 | prompt2 | prompt3')
|
|
400
|
+
return :handled
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
prompts.each do |prompt|
|
|
404
|
+
@message_stream.add_message(role: :user, content: prompt)
|
|
405
|
+
@message_stream.add_message(role: :assistant, content: '')
|
|
406
|
+
send_to_llm(prompt)
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
@message_stream.add_message(
|
|
410
|
+
role: :system,
|
|
411
|
+
content: "Chain complete: #{prompts.size} prompt#{'s' unless prompts.size == 1} sent."
|
|
412
|
+
)
|
|
413
|
+
:handled
|
|
414
|
+
end
|
|
415
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
283
416
|
end
|
|
284
417
|
# rubocop:enable Metrics/ModuleLength
|
|
285
418
|
end
|
|
@@ -248,6 +248,41 @@ module Legion
|
|
|
248
248
|
:handled
|
|
249
249
|
end
|
|
250
250
|
|
|
251
|
+
def handle_replace(input)
|
|
252
|
+
args = input.split(nil, 2)[1]
|
|
253
|
+
unless args&.include?(' >>> ')
|
|
254
|
+
@message_stream.add_message(role: :system, content: 'Usage: /replace old >>> new')
|
|
255
|
+
return :handled
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
parts = args.split(' >>> ', 2)
|
|
259
|
+
count = apply_replace(parts[0], parts[1] || '')
|
|
260
|
+
report_replace_result(count, parts[0], parts[1] || '')
|
|
261
|
+
:handled
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def apply_replace(old_text, new_text)
|
|
265
|
+
count = 0
|
|
266
|
+
@message_stream.messages.each do |msg|
|
|
267
|
+
next unless msg[:content].is_a?(::String) && msg[:content].include?(old_text)
|
|
268
|
+
|
|
269
|
+
count += msg[:content].scan(old_text).size
|
|
270
|
+
msg[:content] = msg[:content].gsub(old_text, new_text)
|
|
271
|
+
end
|
|
272
|
+
count
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def report_replace_result(count, old_text, new_text)
|
|
276
|
+
if count.zero?
|
|
277
|
+
@message_stream.add_message(role: :system, content: "No occurrences of '#{old_text}' found.")
|
|
278
|
+
else
|
|
279
|
+
@message_stream.add_message(
|
|
280
|
+
role: :system,
|
|
281
|
+
content: "Replaced #{count} occurrence#{'s' unless count == 1} of '#{old_text}' with '#{new_text}'."
|
|
282
|
+
)
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
251
286
|
def search_messages(query)
|
|
252
287
|
pattern = query.downcase
|
|
253
288
|
@message_stream.messages.select do |msg|
|
|
@@ -157,6 +157,82 @@ module Legion
|
|
|
157
157
|
nil
|
|
158
158
|
end
|
|
159
159
|
|
|
160
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
161
|
+
def handle_info
|
|
162
|
+
cfg = safe_config
|
|
163
|
+
elapsed = Time.now - @session_start
|
|
164
|
+
hours = (elapsed / 3600).to_i
|
|
165
|
+
minutes = ((elapsed % 3600) / 60).to_i
|
|
166
|
+
seconds = (elapsed % 60).to_i
|
|
167
|
+
uptime_str = "#{hours}h #{minutes}m #{seconds}s"
|
|
168
|
+
|
|
169
|
+
msgs = @message_stream.messages
|
|
170
|
+
counts = %i[user assistant system tool].to_h { |r| [r, msgs.count { |m| m[:role] == r }] }
|
|
171
|
+
total_chars = msgs.sum { |m| m[:content].to_s.length }
|
|
172
|
+
avg_len = (total_chars.to_f / [msgs.size, 1].max).round
|
|
173
|
+
|
|
174
|
+
model_info = if @llm_chat.respond_to?(:model)
|
|
175
|
+
@llm_chat.model.to_s
|
|
176
|
+
else
|
|
177
|
+
cfg[:provider] || 'none'
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
tagged_count = msgs.count { |m| m[:tags]&.any? }
|
|
181
|
+
fav_count = msgs.count { |m| m[:favorited] }
|
|
182
|
+
|
|
183
|
+
lines = [
|
|
184
|
+
"Session: #{@session_name}",
|
|
185
|
+
"Started: #{@session_start.strftime('%Y-%m-%d %H:%M:%S')}",
|
|
186
|
+
"Uptime: #{uptime_str}",
|
|
187
|
+
'',
|
|
188
|
+
"Messages: #{msgs.size} total",
|
|
189
|
+
" User: #{counts[:user]}, Assistant: #{counts[:assistant]}, System: #{counts[:system]}",
|
|
190
|
+
" Tool: #{counts[:tool]}",
|
|
191
|
+
'',
|
|
192
|
+
"Total characters: #{total_chars}",
|
|
193
|
+
"Avg message length: #{avg_len} chars",
|
|
194
|
+
'',
|
|
195
|
+
"Pinned: #{@pinned_messages.size}",
|
|
196
|
+
"Tagged: #{tagged_count}",
|
|
197
|
+
"Favorited: #{fav_count}",
|
|
198
|
+
"Aliases: #{@aliases.size}",
|
|
199
|
+
"Snippets: #{@snippets.size}",
|
|
200
|
+
"Macros: #{@macros.size}",
|
|
201
|
+
'',
|
|
202
|
+
"Autosave: #{@autosave_enabled ? "ON (every #{@autosave_interval}s)" : 'OFF'}",
|
|
203
|
+
"Focus mode: #{@focus_mode ? 'on' : 'off'}",
|
|
204
|
+
"Muted system: #{@muted_system ? 'on' : 'off'}",
|
|
205
|
+
"Plan mode: #{@plan_mode ? 'on' : 'off'}",
|
|
206
|
+
"Debug mode: #{@debug_mode ? 'on' : 'off'}",
|
|
207
|
+
"LLM: #{model_info}"
|
|
208
|
+
]
|
|
209
|
+
@message_stream.add_message(role: :system, content: lines.join("\n"))
|
|
210
|
+
:handled
|
|
211
|
+
end
|
|
212
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
213
|
+
|
|
214
|
+
def handle_reset
|
|
215
|
+
@message_stream.messages.clear
|
|
216
|
+
@plan_mode = false
|
|
217
|
+
@focus_mode = false
|
|
218
|
+
@debug_mode = false
|
|
219
|
+
@muted_system = false
|
|
220
|
+
@pinned_messages = []
|
|
221
|
+
@aliases = {}
|
|
222
|
+
@macros = {}
|
|
223
|
+
@recording_macro = nil
|
|
224
|
+
@macro_buffer = []
|
|
225
|
+
@session_name = 'default'
|
|
226
|
+
@status_bar.update(session: 'default', plan_mode: false, debug_mode: false)
|
|
227
|
+
cfg = safe_config
|
|
228
|
+
@message_stream.add_message(
|
|
229
|
+
role: :system,
|
|
230
|
+
content: "Welcome#{", #{cfg[:name]}" if cfg[:name]}. Type /help for commands."
|
|
231
|
+
)
|
|
232
|
+
@status_bar.notify(message: 'Session reset', level: :info, ttl: 3)
|
|
233
|
+
:handled
|
|
234
|
+
end
|
|
235
|
+
|
|
160
236
|
def handle_merge(input)
|
|
161
237
|
name = input.split(nil, 2)[1]
|
|
162
238
|
unless name
|
|
@@ -313,6 +313,117 @@ module Legion
|
|
|
313
313
|
end
|
|
314
314
|
:handled
|
|
315
315
|
end
|
|
316
|
+
|
|
317
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
318
|
+
def handle_scroll(input)
|
|
319
|
+
arg = input.split(nil, 2)[1]
|
|
320
|
+
unless arg
|
|
321
|
+
pos = @message_stream.scroll_position
|
|
322
|
+
@message_stream.add_message(
|
|
323
|
+
role: :system,
|
|
324
|
+
content: "Scroll position: offset=#{pos[:current]}, messages=#{pos[:total]}"
|
|
325
|
+
)
|
|
326
|
+
return :handled
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
case arg.strip
|
|
330
|
+
when 'top'
|
|
331
|
+
@message_stream.scroll_up(@message_stream.messages.size * 5)
|
|
332
|
+
@message_stream.add_message(role: :system, content: 'Scrolled to top.')
|
|
333
|
+
when 'bottom'
|
|
334
|
+
@message_stream.scroll_down(@message_stream.scroll_offset)
|
|
335
|
+
@message_stream.add_message(role: :system, content: 'Scrolled to bottom.')
|
|
336
|
+
else
|
|
337
|
+
idx = arg.strip.to_i
|
|
338
|
+
if idx >= 0 && idx < @message_stream.messages.size
|
|
339
|
+
@message_stream.scroll_down(@message_stream.scroll_offset)
|
|
340
|
+
target_offset = [@message_stream.messages.size - idx - 1, 0].max
|
|
341
|
+
@message_stream.scroll_up(target_offset)
|
|
342
|
+
@message_stream.add_message(role: :system, content: "Scrolled to message #{idx}.")
|
|
343
|
+
else
|
|
344
|
+
@message_stream.add_message(
|
|
345
|
+
role: :system,
|
|
346
|
+
content: 'Invalid index. Usage: /scroll top|bottom|<N>'
|
|
347
|
+
)
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
:handled
|
|
351
|
+
end
|
|
352
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
353
|
+
|
|
354
|
+
def handle_highlight(input)
|
|
355
|
+
arg = input.split(nil, 2)[1]
|
|
356
|
+
@highlights ||= []
|
|
357
|
+
|
|
358
|
+
unless arg
|
|
359
|
+
@message_stream.add_message(role: :system, content: 'Usage: /highlight <pattern> | clear | list')
|
|
360
|
+
return :handled
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
case arg.strip
|
|
364
|
+
when 'clear' then highlight_clear
|
|
365
|
+
when 'list' then highlight_list
|
|
366
|
+
else highlight_add(arg.strip)
|
|
367
|
+
end
|
|
368
|
+
:handled
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
def highlight_clear
|
|
372
|
+
@highlights = []
|
|
373
|
+
@message_stream.highlights = @highlights
|
|
374
|
+
@message_stream.add_message(role: :system, content: 'Highlights cleared.')
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
def highlight_list
|
|
378
|
+
if @highlights.empty?
|
|
379
|
+
@message_stream.add_message(role: :system, content: 'No active highlights.')
|
|
380
|
+
else
|
|
381
|
+
lines = @highlights.each_with_index.map { |p, i| " #{i + 1}. #{p}" }
|
|
382
|
+
@message_stream.add_message(role: :system,
|
|
383
|
+
content: "Active highlights (#{@highlights.size}):\n#{lines.join("\n")}")
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
def highlight_add(pattern)
|
|
388
|
+
@highlights << pattern
|
|
389
|
+
@message_stream.highlights = @highlights
|
|
390
|
+
@message_stream.add_message(role: :system, content: "Highlight added: '#{pattern}'")
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
394
|
+
def handle_summary
|
|
395
|
+
msgs = @message_stream.messages
|
|
396
|
+
elapsed = Time.now - @session_start
|
|
397
|
+
hours = (elapsed / 3600).to_i
|
|
398
|
+
minutes = ((elapsed % 3600) / 60).to_i
|
|
399
|
+
seconds = (elapsed % 60).to_i
|
|
400
|
+
uptime_str = "#{hours}h #{minutes}m #{seconds}s"
|
|
401
|
+
|
|
402
|
+
counts = %i[user assistant system].to_h { |r| [r, msgs.count { |m| m[:role] == r }] }
|
|
403
|
+
most_active = counts.max_by { |_, v| v }&.first || :none
|
|
404
|
+
|
|
405
|
+
user_msgs = msgs.select { |m| m[:role] == :user }
|
|
406
|
+
top_words = user_msgs.flat_map { |m| m[:content].to_s.split.first(1) }
|
|
407
|
+
.tally.sort_by { |_, c| -c }.first(5).map(&:first)
|
|
408
|
+
|
|
409
|
+
longest = msgs.max_by { |m| m[:content].to_s.length }
|
|
410
|
+
longest_preview = longest ? truncate_text(longest[:content].to_s, 60) : 'none'
|
|
411
|
+
|
|
412
|
+
last_user = user_msgs.last
|
|
413
|
+
recent_topic = last_user ? truncate_text(last_user[:content].to_s, 40) : 'none'
|
|
414
|
+
|
|
415
|
+
lines = [
|
|
416
|
+
'Conversation Summary',
|
|
417
|
+
" Messages: #{msgs.size}, Duration: #{uptime_str}",
|
|
418
|
+
" Most active role: #{most_active}",
|
|
419
|
+
" Top starting words: #{top_words.empty? ? 'none' : top_words.join(', ')}",
|
|
420
|
+
" Longest message: #{longest_preview}",
|
|
421
|
+
" Most recent topic: #{recent_topic}"
|
|
422
|
+
]
|
|
423
|
+
@message_stream.add_message(role: :system, content: lines.join("\n"))
|
|
424
|
+
:handled
|
|
425
|
+
end
|
|
426
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
316
427
|
end
|
|
317
428
|
# rubocop:enable Metrics/ModuleLength
|
|
318
429
|
end
|
|
@@ -31,7 +31,9 @@ module Legion
|
|
|
31
31
|
/context /alias /snippet /debug /uptime /time /bookmark /welcome /tips
|
|
32
32
|
/wc /import /mute /autosave /react /macro /tag /tags /repeat /count
|
|
33
33
|
/template /fav /favs /log /version
|
|
34
|
-
/focus /retry /merge /sort
|
|
34
|
+
/focus /retry /merge /sort
|
|
35
|
+
/chain /info /scroll /summary
|
|
36
|
+
/prompt /reset /replace /highlight].freeze
|
|
35
37
|
|
|
36
38
|
PERSONALITIES = {
|
|
37
39
|
'default' => 'You are Legion, an async cognition engine and AI assistant. Be helpful and concise.',
|
|
@@ -71,6 +73,7 @@ module Legion
|
|
|
71
73
|
@last_command = nil
|
|
72
74
|
@focus_mode = false
|
|
73
75
|
@last_user_input = nil
|
|
76
|
+
@highlights = []
|
|
74
77
|
end
|
|
75
78
|
|
|
76
79
|
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
@@ -405,6 +408,14 @@ module Legion
|
|
|
405
408
|
when '/retry' then handle_retry
|
|
406
409
|
when '/merge' then handle_merge(input)
|
|
407
410
|
when '/sort' then handle_sort(input)
|
|
411
|
+
when '/chain' then handle_chain(input)
|
|
412
|
+
when '/info' then handle_info
|
|
413
|
+
when '/scroll' then handle_scroll(input)
|
|
414
|
+
when '/summary' then handle_summary
|
|
415
|
+
when '/prompt' then handle_prompt(input)
|
|
416
|
+
when '/reset' then handle_reset
|
|
417
|
+
when '/replace' then handle_replace(input)
|
|
418
|
+
when '/highlight' then handle_highlight(input)
|
|
408
419
|
else :handled
|
|
409
420
|
end
|
|
410
421
|
end
|
data/lib/legion/tty/version.rb
CHANGED