legion-tty 0.4.24 → 0.4.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ab1a18bdbec9078051e19b84ede94cdaacb600ed12006674dc5696f711230349
4
- data.tar.gz: 6735876baede0f4fd45637f6a5cb0cef4284f7117298fa96cb97c8ca667a23d4
3
+ metadata.gz: 8afc2aa24b5ae5e56f84d7f7ff538e9d0ccc0b9274fd983cec26116fb234b774
4
+ data.tar.gz: 2d442f8ca2ec7a2624f4f3de63defb569c3b324babfff9e8f98c9540bef984d3
5
5
  SHA512:
6
- metadata.gz: '01294f74d7805fb5aad66aa1538ffe38cb1f8b407aaaa662d90cacaf7f8c0b4e787257636007f8a74e88e1e13b76a5432da4a2ff47aad0dd5b6f81852116ddfb'
7
- data.tar.gz: 789298565c5285d7ef1787fd7dfbf8034b181f57f3cab243cd4b424712b0ec7251836e8c8f5ff3b79faf4b0d98c5416e919e8cf80c42df09adbe443d5b77905f
6
+ metadata.gz: f1cd31e1a7fff97bd1de1088247fa1c195c27beaaef7f7d94df5e0ebaf6e46bca7c31a55c2321956c9fbbb46a88e862b6db6139f5df54b104785e1061766b37e
7
+ data.tar.gz: f38bec5c06b804f6b74fc103804a7b46cb9a9b762ec73c4eac3409d0ea72db235b69638b93cd8342a9052d4a5eb20399498b05e0cda59a303c911c4b08e734f2
data/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.26] - 2026-03-19
4
+
5
+ ### Added
6
+ - `/goto <N>` command: jump to specific message by index
7
+ - `/inject <role> <text>` command: inject a message with specific role (user/assistant/system)
8
+ - `/stopwatch [start|stop|lap|reset]` command: built-in stopwatch with MM:SS.ms format
9
+ - `/ago <N>` command: show what was said N messages ago
10
+
11
+ ### Changed
12
+ - Updated README.md and CLAUDE.md to reflect 107 slash commands, 1732 specs, 146 files
13
+
14
+ ## [0.4.25] - 2026-03-19
15
+
16
+ ### Added
17
+ - `/ask <question>` command: quick concise Q&A mode (instructs LLM to answer in one paragraph)
18
+ - `/define <term>` command: ask LLM for a concise definition
19
+ - `/status` command: comprehensive view of all 18 toggleable modes and settings
20
+ - `/prefs [key] [value]` command: persistent user preferences in `~/.legionio/prefs.json`
21
+ - `/about` command: show legion-tty name, version, author, license, GitHub URL
22
+ - `/commands [pattern]` command: list all slash commands with optional pattern filter and count
23
+
24
+ ### Changed
25
+ - Total slash commands: 103 (milestone)
26
+
3
27
  ## [0.4.24] - 2026-03-19
4
28
 
5
29
  ### 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.18
5
+ **Version**: 0.4.25
6
6
 
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.
7
+ Think Claude Code meets Codex CLI, but for LegionIO: onboarding wizard with identity detection, streaming AI chat shell with 103 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 60 slash commands, tab completion, markdown rendering, and tool panels
13
+ - **AI chat shell** - Streaming LLM chat with 103 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
@@ -35,6 +35,26 @@ Think Claude Code meets Codex CLI, but for LegionIO: onboarding wizard with iden
35
35
  - **Animated spinner** - Status bar spinner during LLM thinking
36
36
  - **Daemon routing** - Routes through LegionIO daemon when available, falls back to direct
37
37
  - **Second-run flow** - Skips onboarding, re-scans environment, drops into chat
38
+ - **Session chaining** - `/chain` sends pipe-separated prompts sequentially; `/info` shows full session state
39
+ - **Scroll navigation** - `/scroll`, `/top`, `/bottom`, `/head`, `/tail` for precise history navigation
40
+ - **Conversation editing** - `/replace`, `/reset`, `/prompt`, `/highlight` for post-hoc message editing
41
+ - **Multi-line input** - `/multiline` toggles submit-on-empty-line mode with `[ML]` status indicator
42
+ - **Message annotations** - `/annotate` and `/annotations` for inline notes on specific messages
43
+ - **Message filtering** - `/filter` by role, tag, or pinned status
44
+ - **Export YAML** - `/export yaml` alongside existing md/json/html formats
45
+ - **Session archiving** - `/archive` moves session to `~/.legionio/archives/` and starts fresh
46
+ - **Shell integration** - `/tee` mirrors messages to a file in real-time; `/pipe` pipes output through a shell command
47
+ - **Math utilities** - `/calc` for safe expression evaluation; `/rand` for random number generation
48
+ - **Shell-like commands** - `/ls`, `/pwd`, `/echo`, `/env` for quick filesystem and environment inspection
49
+ - **Display controls** - `/wrap`, `/number`, `/color`, `/timestamps`, `/truncate`, `/silent`, `/speak`
50
+ - **Draft buffer** - `/draft` and `/revise` for composing and editing messages before sending
51
+ - **Word frequency** - `/freq` shows top-20 words in conversation (excludes stop words)
52
+ - **Named markers** - `/mark` inserts named bookmarks; list all markers with `/mark`
53
+ - **Persistent preferences** - `/prefs` reads/writes `~/.legionio/prefs.json` across sessions
54
+ - **Quick Q&A** - `/ask` and `/define` for concise one-paragraph LLM answers
55
+ - **Status overview** - `/status` shows all 18 toggleable modes and settings at a glance
56
+ - **Command discovery** - `/commands [pattern]` lists all slash commands with optional pattern filter
57
+ - **About info** - `/about` shows gem name, version, author, license, and GitHub URL
38
58
 
39
59
  ## Installation
40
60
 
@@ -76,64 +96,108 @@ legion chat prompt "explain async cognition"
76
96
 
77
97
  | Command | Description |
78
98
  |---------|-------------|
79
- | `/help` | Show all commands and hotkeys |
80
- | `/quit` | Exit (auto-saves session) |
99
+ | `/about` | Show gem name, version, author, license, and GitHub URL |
100
+ | `/alias [name] [cmd]` | Create or list command aliases |
101
+ | `/annotate [N] <text>` | Add a note to a specific message |
102
+ | `/annotations` | List all annotated messages with their notes |
103
+ | `/archive [name]` | Archive session to `~/.legionio/archives/` and start fresh |
104
+ | `/archives` | List all archived sessions with file sizes |
105
+ | `/ask <question>` | Quick concise Q&A mode (LLM answers in one paragraph) |
106
+ | `/autosave [N\|off]` | Toggle periodic auto-save with configurable interval |
107
+ | `/bookmark` | Export pinned messages to a markdown file |
108
+ | `/bottom` | Scroll to bottom of message history |
109
+ | `/calc <expression>` | Evaluate a math expression (supports Math functions) |
110
+ | `/chain <p1\|p2\|...>` | Send pipe-separated prompts to LLM sequentially |
81
111
  | `/clear` | Clear message history |
82
- | `/model <name>` | Switch LLM model at runtime |
83
- | `/session <name>` | Set session name |
112
+ | `/color [on\|off]` | Toggle colorized output (strips ANSI codes when off) |
113
+ | `/commands [pattern]` | List all slash commands with optional pattern filter |
114
+ | `/compact [N]` | Keep last N message pairs, remove older |
115
+ | `/config` | View and edit `~/.legionio/settings/*.json` files |
116
+ | `/context` | Show active session state summary |
117
+ | `/copy` | Copy last assistant response to clipboard |
84
118
  | `/cost` | Show token usage and estimated cost |
85
- | `/export [md\|json\|html]` | Export chat history to file |
86
- | `/tools` | List discovered LEX extensions |
119
+ | `/count <pattern>` | Count messages matching a pattern with per-role breakdown |
87
120
  | `/dashboard` | Toggle operational dashboard |
88
- | `/hotkeys` | Show registered hotkey bindings |
89
- | `/save [name]` | Save current session |
90
- | `/load <name>` | Load a saved session |
91
- | `/sessions` | List all saved sessions |
92
- | `/system <prompt>` | Set or override system prompt |
121
+ | `/debug` | Toggle debug mode |
122
+ | `/define <term>` | Ask LLM for a concise definition |
93
123
  | `/delete <session>` | Delete a saved session |
94
- | `/plan` | Toggle read-only bookmark mode |
95
- | `/palette` | Open command palette (fuzzy search) |
124
+ | `/diff` | Show new messages since last session load |
125
+ | `/draft [text\|send\|clear]` | Save text to draft buffer, show, send, or clear it |
126
+ | `/echo <text>` | Add a user-defined system message note |
127
+ | `/env` | Show environment info (Ruby version, platform, terminal, PID) |
128
+ | `/export [md\|json\|html\|yaml]` | Export chat history to file |
96
129
  | `/extensions` | Browse installed LEX extensions |
97
- | `/config` | View and edit settings files |
98
- | `/theme [name]` | Switch color theme (purple/green/blue/amber) |
99
- | `/search <text>` | Search message history |
130
+ | `/fav [N]` | Favorite a message (persists to `~/.legionio/favorites.json`) |
131
+ | `/favs` | Show all favorited messages |
132
+ | `/filter [role\|tag\|pinned\|clear]` | Filter displayed messages |
133
+ | `/focus` | Toggle minimal UI (hide status bar) |
134
+ | `/freq` | Word frequency analysis with top 20 words (excludes stop words) |
100
135
  | `/grep <pattern>` | Regex search across messages |
101
- | `/compact [N]` | Keep last N message pairs, remove older |
102
- | `/copy` | Copy last assistant response to clipboard |
103
- | `/diff` | Show new messages since last session load |
104
- | `/stats` | Show conversation statistics |
105
- | `/personality [style]` | Switch personality (default/concise/detailed/friendly/technical) |
106
- | `/undo` | Remove last user+assistant message pair |
136
+ | `/head [N]` | Peek at first N messages (default 5) |
137
+ | `/help` | Show all commands and hotkeys |
138
+ | `/highlight <pattern>` | Highlight text patterns in message rendering |
107
139
  | `/history` | Show input history |
140
+ | `/hotkeys` | Show registered hotkey bindings |
141
+ | `/import <path>` | Import session from a JSON file |
142
+ | `/info` | Comprehensive session info (modes, counts, aliases, macros, provider) |
143
+ | `/load <name>` | Load a saved session |
144
+ | `/log [N]` | View last N lines of boot log (default 20) |
145
+ | `/ls [path]` | List directory contents |
146
+ | `/macro <action>` | Record/stop/play/list/delete command macros |
147
+ | `/mark <label>` | Insert a named marker/bookmark in conversation |
148
+ | `/merge <session>` | Merge another session into current |
149
+ | `/model <name>` | Switch LLM model at runtime |
150
+ | `/multiline` | Toggle multi-line input mode (submit with empty line) |
151
+ | `/mute` | Toggle system message display |
152
+ | `/number [on\|off]` | Toggle message numbering with `[N]` prefix |
153
+ | `/palette` | Open command palette (fuzzy search) |
154
+ | `/personality [style]` | Switch personality (default/concise/detailed/friendly/technical) |
108
155
  | `/pin [N]` | Pin a message (last assistant or by index) |
109
156
  | `/pins` | Show all pinned messages |
157
+ | `/pipe <command>` | Pipe last assistant response through a shell command |
158
+ | `/plan` | Toggle read-only bookmark mode |
159
+ | `/prefs [key] [value]` | Read or write persistent user preferences |
160
+ | `/prompt save\|load\|list\|delete` | Persist and reuse custom system prompts |
161
+ | `/pwd` | Show current working directory |
162
+ | `/quit` | Exit (auto-saves session) |
163
+ | `/rand [N\|min..max]` | Generate random numbers (float, integer, or range) |
164
+ | `/react <emoji>` | Add emoji reaction to a message |
110
165
  | `/rename <name>` | Rename current session (moves saved file) |
111
- | `/context` | Show active session state summary |
112
- | `/alias [name] [cmd]` | Create or list command aliases |
166
+ | `/repeat` | Re-execute the last slash command |
167
+ | `/replace old >>> new` | Find and replace text across all messages |
168
+ | `/reset` | Reset session to clean state |
169
+ | `/retry` | Resend last user message to LLM |
170
+ | `/revise <text>` | Replace the content of the last user message |
171
+ | `/save [name]` | Save current session |
172
+ | `/scroll [top\|bottom\|N]` | Navigate to a specific scroll position |
173
+ | `/search <text>` | Search message history |
174
+ | `/session <name>` | Set session name |
175
+ | `/sessions` | List all saved sessions |
176
+ | `/silent` | Toggle silent mode (responses tracked but not displayed) |
113
177
  | `/snippet <action>` | Save/load/list/delete code snippets |
114
- | `/debug` | Toggle debug mode |
115
- | `/uptime` | Show session elapsed time |
116
- | `/bookmark` | Export pinned messages to file |
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 |
178
+ | `/sort [length\|role]` | Show messages sorted by length or grouped by role |
179
+ | `/speak [on\|off]` | Toggle text-to-speech for assistant messages (macOS only) |
180
+ | `/stats` | Show conversation statistics |
181
+ | `/status` | Show all 18 toggleable modes and settings |
182
+ | `/summary` | Generate a local conversation summary |
183
+ | `/system <prompt>` | Set or override system prompt |
121
184
  | `/tag <label>` | Tag a message with a label |
122
185
  | `/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 |
186
+ | `/tail [N]` | Peek at last N messages (default 5) |
187
+ | `/tee <path>` | Copy new messages to a file in real-time |
188
+ | `/template [name]` | List or use prompt templates (8 built-in) |
189
+ | `/theme [name]` | Switch color theme (purple/green/blue/amber) |
190
+ | `/time` | Show current date and time |
191
+ | `/timestamps [on\|off]` | Toggle timestamp display on messages |
192
+ | `/tools` | List discovered LEX extensions |
193
+ | `/top` | Scroll to top of message history |
194
+ | `/truncate [N\|off]` | Display-only truncation of long messages |
195
+ | `/undo` | Remove last user+assistant message pair |
196
+ | `/uptime` | Show session elapsed time |
129
197
  | `/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 |
198
+ | `/wc` | Show word count statistics per role |
199
+ | `/welcome` | Redisplay the welcome message |
200
+ | `/wrap [N\|off]` | Set custom word wrap width |
137
201
 
138
202
  ## Hotkeys
139
203
 
@@ -158,13 +222,13 @@ legion-tty
158
222
 
159
223
  Screens/
160
224
  Onboarding # First-run wizard (rain -> intro -> wizard -> reveal)
161
- Chat # AI chat REPL with streaming + 60 slash commands
162
- SessionCommands # save/load/sessions/delete/rename
163
- ExportCommands # export/bookmark/html/json/markdown
164
- MessageCommands # compact/copy/diff/search/grep/undo/pin/pins
165
- UiCommands # help/clear/dashboard/hotkeys/palette/context/stats/debug/history/uptime/time
166
- ModelCommands # model/system/personality switching
167
- CustomCommands # alias/snippet management
225
+ Chat # AI chat REPL with streaming + 103 slash commands
226
+ SessionCommands # save/load/sessions/delete/rename/import/merge/autosave
227
+ ExportCommands # export/bookmark/html/json/markdown/yaml
228
+ MessageCommands # compact/copy/diff/search/grep/undo/pin/pins/react/tag/fav/sort/count
229
+ UiCommands # help/clear/dashboard/hotkeys/palette/context/stats/debug/history/uptime/time/tips/welcome/focus/wc/log/version/mute + calc/rand/mark/freq/color/timestamps/top/bottom/head/tail/echo/env/speak/silent/wrap/number/truncate/about/commands/ask/define/status/prefs
230
+ ModelCommands # model/system/personality switching/retry/chain/info/scroll/summary/prompt/reset/replace/highlight/multiline/filter/annotate/annotations
231
+ CustomCommands # alias/snippet/template/macro/draft/revise/tee/pipe/archive/archives/ls/pwd/rand/calc
168
232
  Dashboard # Service/LLM status, panel navigation (j/k/1-5)
169
233
  Extensions # LEX gem browser by category with homepage opener
170
234
  Config # Settings file viewer/editor with JSON validation
@@ -213,8 +277,8 @@ Boot logs go to `~/.legionio/logs/tty-boot.log`.
213
277
 
214
278
  ```bash
215
279
  bundle install
216
- bundle exec rspec # 1143 examples, 0 failures
217
- bundle exec rubocop # 106 files, 0 offenses
280
+ bundle exec rspec # 1687 examples, 0 failures
281
+ bundle exec rubocop # 143 files, 0 offenses
218
282
  ```
219
283
 
220
284
  ## License
@@ -5,6 +5,8 @@ module Legion
5
5
  module Screens
6
6
  class Chat < Base
7
7
  module MessageCommands
8
+ INJECT_VALID_ROLES = %w[user assistant system].freeze
9
+
8
10
  private
9
11
 
10
12
  # rubocop:disable Metrics/AbcSize
@@ -655,6 +657,46 @@ module Legion
655
657
  @message_stream.add_message(role: :system, content: "Revised: #{new_content}")
656
658
  :handled
657
659
  end
660
+
661
+ def handle_inject(input)
662
+ parts = input.split(nil, 3)
663
+ role_str = parts[1]
664
+ text = parts[2]
665
+
666
+ unless role_str && INJECT_VALID_ROLES.include?(role_str) && text && !text.strip.empty?
667
+ @message_stream.add_message(
668
+ role: :system,
669
+ content: 'Usage: /inject <user|assistant|system> <text>'
670
+ )
671
+ return :handled
672
+ end
673
+
674
+ @message_stream.add_message(role: role_str.to_sym, content: text.strip)
675
+ @status_bar.update(message_count: @message_stream.messages.size)
676
+ @message_stream.add_message(role: :system, content: "Injected [#{role_str}] message.")
677
+ :handled
678
+ end
679
+
680
+ def handle_ago(input)
681
+ n = (input.split(nil, 2)[1] || '1').to_i
682
+ msgs = @message_stream.messages
683
+ if n < 1 || n > msgs.size
684
+ @message_stream.add_message(
685
+ role: :system,
686
+ content: "No message #{n} ago (conversation has #{msgs.size} message(s))."
687
+ )
688
+ return :handled
689
+ end
690
+
691
+ idx = msgs.size - n
692
+ msg = msgs[idx]
693
+ preview = truncate_text(msg[:content].to_s, 200)
694
+ @message_stream.add_message(
695
+ role: :system,
696
+ content: "[#{n} ago | ##{idx} | #{msg[:role]}] #{preview}"
697
+ )
698
+ :handled
699
+ end
658
700
  end
659
701
  end
660
702
  end
@@ -154,6 +154,32 @@ module Legion
154
154
  end
155
155
  :handled
156
156
  end
157
+
158
+ def handle_ask(input)
159
+ question = input.split(nil, 2)[1]&.strip
160
+ unless question && !question.empty?
161
+ @message_stream.add_message(role: :system, content: 'Usage: /ask <question>')
162
+ return :handled
163
+ end
164
+
165
+ @message_stream.add_message(role: :user, content: question)
166
+ @message_stream.add_message(role: :assistant, content: '')
167
+ send_to_llm("Answer the following question concisely in one paragraph: #{question}")
168
+ :handled
169
+ end
170
+
171
+ def handle_define(input)
172
+ term = input.split(nil, 2)[1]&.strip
173
+ unless term && !term.empty?
174
+ @message_stream.add_message(role: :system, content: 'Usage: /define <term>')
175
+ return :handled
176
+ end
177
+
178
+ @message_stream.add_message(role: :user, content: term)
179
+ @message_stream.add_message(role: :assistant, content: '')
180
+ send_to_llm("Define the following term concisely: #{term}")
181
+ :handled
182
+ end
157
183
  end
158
184
  end
159
185
  end
@@ -393,6 +393,37 @@ module Legion
393
393
  end
394
394
  # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
395
395
 
396
+ def handle_goto(input)
397
+ n_str = input.split(nil, 2)[1]
398
+ unless n_str&.match?(/\A\d+\z/)
399
+ @message_stream.add_message(role: :system, content: 'Usage: /goto <N>')
400
+ return :handled
401
+ end
402
+
403
+ idx = n_str.to_i
404
+ return goto_out_of_range(idx) unless goto_in_range?(idx)
405
+
406
+ scroll_to_message(idx)
407
+ @message_stream.add_message(role: :system, content: "Jumped to message #{idx}.")
408
+ :handled
409
+ end
410
+
411
+ def goto_in_range?(idx)
412
+ idx >= 0 && idx < @message_stream.messages.size
413
+ end
414
+
415
+ def goto_out_of_range(idx)
416
+ total = @message_stream.messages.size
417
+ @message_stream.add_message(role: :system, content: "Message #{idx} out of range (0..#{total - 1}).")
418
+ :handled
419
+ end
420
+
421
+ def scroll_to_message(idx)
422
+ total = @message_stream.messages.size
423
+ @message_stream.scroll_down(@message_stream.scroll_offset)
424
+ @message_stream.scroll_up([total - idx - 1, 0].max)
425
+ end
426
+
396
427
  def handle_highlight(input)
397
428
  arg = input.split(nil, 2)[1]
398
429
  @highlights ||= []
@@ -680,6 +711,49 @@ module Legion
680
711
  :handled
681
712
  end
682
713
 
714
+ def handle_about
715
+ lines = [
716
+ 'legion-tty',
717
+ 'Description : Rich terminal UI for the LegionIO async cognition engine',
718
+ "Version : #{Legion::TTY::VERSION}",
719
+ 'Author : Matthew Iverson (@Esity)',
720
+ 'License : Apache-2.0',
721
+ 'GitHub : https://github.com/LegionIO/legion-tty'
722
+ ]
723
+ @message_stream.add_message(role: :system, content: lines.join("\n"))
724
+ :handled
725
+ end
726
+
727
+ def handle_commands(input)
728
+ pattern = input.split(nil, 2)[1]&.strip&.downcase
729
+ cmds = filter_commands(pattern)
730
+ header = commands_header(pattern, cmds)
731
+ rows = format_command_columns(cmds)
732
+ @message_stream.add_message(role: :system, content: "#{header}\n#{rows.join("\n")}")
733
+ :handled
734
+ end
735
+
736
+ def filter_commands(pattern)
737
+ cmds = SLASH_COMMANDS.sort
738
+ return cmds unless pattern && !pattern.empty?
739
+
740
+ cmds.select { |c| c.include?(pattern) }
741
+ end
742
+
743
+ def commands_header(pattern, cmds)
744
+ if pattern && !pattern.empty?
745
+ "Commands matching '#{pattern}' (#{cmds.size}):"
746
+ else
747
+ "All commands (#{cmds.size}):"
748
+ end
749
+ end
750
+
751
+ def format_command_columns(cmds)
752
+ col_width = cmds.map(&:length).max.to_i + 2
753
+ cols = [terminal_width / [col_width, 1].max, 1].max
754
+ cmds.each_slice(cols).map { |row| row.map { |c| c.ljust(col_width) }.join.rstrip }
755
+ end
756
+
683
757
  def handle_freq
684
758
  words = collect_freq_words
685
759
  if words.empty?
@@ -707,6 +781,169 @@ module Legion
707
781
  format(FREQ_ROW_FMT, rank: rank, word: word, count: count, pct: pct)
708
782
  end
709
783
  end
784
+
785
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
786
+ def handle_status
787
+ autosave_val = @autosave_enabled ? "on (every #{@autosave_interval}s)" : 'off'
788
+ filter_val = @message_stream.filter ? @message_stream.filter[:type].to_s : 'none'
789
+ wrap_val = @message_stream.wrap_width ? @message_stream.wrap_width.to_s : 'off'
790
+ truncate_val = @message_stream.truncate_limit ? @message_stream.truncate_limit.to_s : 'off'
791
+ tee_val = @tee_path || 'off'
792
+ lines = [
793
+ 'Mode Status:',
794
+ " Plan mode : #{@plan_mode ? 'on' : 'off'}",
795
+ " Focus mode : #{@focus_mode ? 'on' : 'off'}",
796
+ " Debug mode : #{@debug_mode ? 'on' : 'off'}",
797
+ " Silent mode: #{@silent_mode ? 'on' : 'off'}",
798
+ " Mute system: #{@muted_system ? 'on' : 'off'}",
799
+ " Multi-line : #{@multiline_mode ? 'on' : 'off'}",
800
+ " Speak mode : #{defined?(@speak_mode) && @speak_mode ? 'on' : 'off'}",
801
+ " Autosave : #{autosave_val}",
802
+ " Color : #{@message_stream.colorize ? 'on' : 'off'}",
803
+ " Timestamps : #{@message_stream.show_timestamps ? 'on' : 'off'}",
804
+ " Numbers : #{@message_stream.show_numbers ? 'on' : 'off'}",
805
+ " Wrap : #{wrap_val}",
806
+ " Truncate : #{truncate_val}",
807
+ " Filter : #{filter_val}",
808
+ " Tee : #{tee_val}",
809
+ " Theme : #{Theme.current_theme}",
810
+ " Personality: #{@personality || 'default'}"
811
+ ]
812
+ @message_stream.add_message(role: :system, content: lines.join("\n"))
813
+ :handled
814
+ end
815
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
816
+
817
+ def handle_prefs(input)
818
+ parts = input.split(nil, 3)
819
+ key = parts[1]
820
+ value = parts[2]
821
+ if key.nil?
822
+ show_all_prefs
823
+ elsif value.nil?
824
+ show_one_pref(key)
825
+ else
826
+ set_pref(key, value)
827
+ end
828
+ :handled
829
+ end
830
+
831
+ def prefs_path
832
+ File.expand_path('~/.legionio/prefs.json')
833
+ end
834
+
835
+ def load_prefs
836
+ return {} unless File.exist?(prefs_path)
837
+
838
+ require 'json'
839
+ ::JSON.parse(File.read(prefs_path))
840
+ rescue ::JSON::ParserError
841
+ {}
842
+ end
843
+
844
+ def save_prefs(prefs)
845
+ require 'fileutils'
846
+ require 'json'
847
+ FileUtils.mkdir_p(File.dirname(prefs_path))
848
+ File.write(prefs_path, ::JSON.pretty_generate(prefs))
849
+ end
850
+
851
+ def show_all_prefs
852
+ prefs = load_prefs
853
+ if prefs.empty?
854
+ @message_stream.add_message(role: :system, content: 'No preferences saved.')
855
+ else
856
+ lines = prefs.map { |k, v| " #{k}: #{v}" }
857
+ @message_stream.add_message(role: :system, content: "Preferences:\n#{lines.join("\n")}")
858
+ end
859
+ end
860
+
861
+ def show_one_pref(key)
862
+ val = load_prefs[key]
863
+ if val.nil?
864
+ @message_stream.add_message(role: :system, content: "No preference set for '#{key}'.")
865
+ else
866
+ @message_stream.add_message(role: :system, content: "#{key}: #{val}")
867
+ end
868
+ end
869
+
870
+ def set_pref(key, value)
871
+ prefs = load_prefs
872
+ prefs[key] = value
873
+ save_prefs(prefs)
874
+ apply_pref(key, value)
875
+ @message_stream.add_message(role: :system, content: "Preference set: #{key} = #{value}")
876
+ end
877
+
878
+ def apply_pref(key, value)
879
+ case key
880
+ when 'theme' then handle_theme("/theme #{value}")
881
+ when 'personality' then handle_personality("/personality #{value}")
882
+ when 'color' then handle_color("/color #{value}")
883
+ when 'timestamps' then handle_timestamps("/timestamps #{value}")
884
+ end
885
+ end
886
+
887
+ def handle_stopwatch(input)
888
+ sub = input.split(nil, 2)[1]&.strip
889
+ case sub
890
+ when 'start' then stopwatch_start
891
+ when 'stop' then stopwatch_stop
892
+ when 'lap' then stopwatch_lap
893
+ when 'reset' then stopwatch_reset
894
+ else stopwatch_status
895
+ end
896
+ :handled
897
+ end
898
+
899
+ def stopwatch_start
900
+ @stopwatch_start = Time.now
901
+ @message_stream.add_message(role: :system, content: 'Stopwatch started.')
902
+ end
903
+
904
+ def stopwatch_stop
905
+ unless @stopwatch_start
906
+ @message_stream.add_message(role: :system, content: 'Stopwatch is not running.')
907
+ return
908
+ end
909
+
910
+ @stopwatch_elapsed += Time.now - @stopwatch_start
911
+ @stopwatch_start = nil
912
+ @message_stream.add_message(role: :system,
913
+ content: "Stopwatch stopped. Elapsed: #{format_stopwatch(@stopwatch_elapsed)}")
914
+ end
915
+
916
+ def stopwatch_lap
917
+ total = @stopwatch_elapsed
918
+ total += Time.now - @stopwatch_start if @stopwatch_start
919
+ @message_stream.add_message(role: :system, content: "Lap: #{format_stopwatch(total)}")
920
+ end
921
+
922
+ def stopwatch_reset
923
+ @stopwatch_start = nil
924
+ @stopwatch_elapsed = 0
925
+ @message_stream.add_message(role: :system, content: 'Stopwatch reset.')
926
+ end
927
+
928
+ def stopwatch_status
929
+ if @stopwatch_start
930
+ total = @stopwatch_elapsed + (Time.now - @stopwatch_start)
931
+ @message_stream.add_message(role: :system, content: "Stopwatch running: #{format_stopwatch(total)}")
932
+ elsif @stopwatch_elapsed.positive?
933
+ @message_stream.add_message(role: :system,
934
+ content: "Stopwatch stopped at: #{format_stopwatch(@stopwatch_elapsed)}")
935
+ else
936
+ @message_stream.add_message(role: :system, content: 'Stopwatch: 00:00.000 (not started)')
937
+ end
938
+ end
939
+
940
+ def format_stopwatch(seconds)
941
+ total_ms = (seconds * 1000).to_i
942
+ ms = total_ms % 1000
943
+ secs = (total_ms / 1000) % 60
944
+ mins = total_ms / 60_000
945
+ format('%<mins>02d:%<secs>02d.%<ms>03d', mins: mins, secs: secs, ms: ms)
946
+ end
710
947
  end
711
948
  end
712
949
  end
@@ -45,7 +45,12 @@ module Legion
45
45
  /color /timestamps
46
46
  /top /bottom /head /tail
47
47
  /draft /revise
48
- /mark /freq].freeze
48
+ /mark /freq
49
+ /about /commands
50
+ /ask /define
51
+ /status /prefs
52
+ /stopwatch /ago
53
+ /goto /inject].freeze
49
54
 
50
55
  PERSONALITIES = {
51
56
  'default' => 'You are Legion, an async cognition engine and AI assistant. Be helpful and concise.',
@@ -90,6 +95,8 @@ module Legion
90
95
  @speak_mode = false
91
96
  @silent_mode = false
92
97
  @draft = nil
98
+ @stopwatch_start = nil
99
+ @stopwatch_elapsed = 0
93
100
  end
94
101
 
95
102
  # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
@@ -492,6 +499,16 @@ module Legion
492
499
  when '/revise' then handle_revise(input)
493
500
  when '/mark' then handle_mark(input)
494
501
  when '/freq' then handle_freq
502
+ when '/about' then handle_about
503
+ when '/commands' then handle_commands(input)
504
+ when '/ask' then handle_ask(input)
505
+ when '/define' then handle_define(input)
506
+ when '/status' then handle_status
507
+ when '/prefs' then handle_prefs(input)
508
+ when '/stopwatch' then handle_stopwatch(input)
509
+ when '/ago' then handle_ago(input)
510
+ when '/goto' then handle_goto(input)
511
+ when '/inject' then handle_inject(input)
495
512
  else :handled
496
513
  end
497
514
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module TTY
5
- VERSION = '0.4.24'
5
+ VERSION = '0.4.26'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: legion-tty
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.24
4
+ version: 0.4.26
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity