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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bc958211b7004e0392a9e96cf2a874d6c54d6f0c6f183d87d80f428e16d3b014
|
|
4
|
+
data.tar.gz: 13264f1761391724054983f250767d9308aab1a397d0dc1577292268a0f7e031
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fd92069e863ebb5bd1185068adf3f50323fb276a2ae0479a8253e0220c1e4bc22a76abb7b8eb73a4c4a22c84be9f22cddf6eea22d510d7772fd459d3d74a1ff9
|
|
7
|
+
data.tar.gz: afcce634192c2e16f133b0d237cd3720536dfce07e2cae17089f208867546d82d75a58bb352887da1d0d29c0e651d047b79596fea0428d35f152e48f17707069
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.4.13] - 2026-03-19
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Help overlay: `/help` now renders as a categorized overlay (SESSION/CHAT/LLM/NAV/DISPLAY/TOOLS) via screen manager
|
|
7
|
+
- Session message count in status bar ("N msgs" segment)
|
|
8
|
+
- `/welcome` command: redisplay the welcome message
|
|
9
|
+
- `/tips` command: show random usage tips (15 tips covering commands, hotkeys, features)
|
|
10
|
+
|
|
11
|
+
## [0.4.12] - 2026-03-19
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- `/grep <pattern>` command: regex search across message history (case-insensitive, with RegexpError handling)
|
|
15
|
+
- `/time` command: display current date, time, and timezone
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- Refactored Chat screen into 6 concern modules (chat.rb 1220 -> 466 lines):
|
|
19
|
+
- `chat/session_commands.rb` — save/load/sessions/delete/rename
|
|
20
|
+
- `chat/export_commands.rb` — export/bookmark/html/json/markdown
|
|
21
|
+
- `chat/message_commands.rb` — compact/copy/diff/search/grep/undo/pin/pins
|
|
22
|
+
- `chat/ui_commands.rb` — help/clear/dashboard/hotkeys/palette/context/stats/debug/history/uptime/time
|
|
23
|
+
- `chat/model_commands.rb` — model/system/personality switching
|
|
24
|
+
- `chat/custom_commands.rb` — alias/snippet management
|
|
25
|
+
|
|
3
26
|
## [0.4.11] - 2026-03-19
|
|
4
27
|
|
|
5
28
|
### Added
|
data/README.md
CHANGED
|
@@ -2,30 +2,36 @@
|
|
|
2
2
|
|
|
3
3
|
Rich terminal UI for the LegionIO async cognition engine.
|
|
4
4
|
|
|
5
|
-
**Version**: 0.4.
|
|
5
|
+
**Version**: 0.4.12
|
|
6
6
|
|
|
7
|
-
Think Claude Code meets Codex CLI, but for LegionIO: onboarding wizard with identity detection, streaming AI chat shell, 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 40 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
|
|
14
|
-
- **Operational dashboard** - Service status, extension inventory, system info,
|
|
15
|
-
- **Extensions browser** - Browse installed LEX gems by category
|
|
16
|
-
- **Config viewer/editor** - View and edit `~/.legionio/settings/*.json` with vault:// masking
|
|
13
|
+
- **AI chat shell** - Streaming LLM chat with 40 slash commands, tab completion, markdown rendering, and tool panels
|
|
14
|
+
- **Operational dashboard** - Service/LLM status, extension inventory, system info, panel navigation (Ctrl+D or `/dashboard`)
|
|
15
|
+
- **Extensions browser** - Browse installed LEX gems by category with detail view and homepage opener ('o' key)
|
|
16
|
+
- **Config viewer/editor** - View and edit `~/.legionio/settings/*.json` with vault:// masking and JSON validation
|
|
17
17
|
- **Command palette** - Fuzzy-search overlay for all commands, screens, and sessions (Ctrl+K or `/palette`)
|
|
18
18
|
- **Model picker** - Switch LLM providers interactively
|
|
19
|
-
- **Session management** - Auto-save on quit, `/save`, `/load`, `/sessions`, session picker (Ctrl+S)
|
|
19
|
+
- **Session management** - Auto-save on quit, `/save`, `/load`, `/sessions`, `/rename`, session picker (Ctrl+S)
|
|
20
20
|
- **Token tracking** - Per-model pricing for 9 models across 8 providers via `/cost`
|
|
21
21
|
- **Plan mode** - Bookmark messages without sending to LLM (`/plan`)
|
|
22
22
|
- **Personality styles** - Switch between default, concise, detailed, friendly, technical (`/personality`)
|
|
23
23
|
- **Theme selection** - Four built-in themes: purple (default), green, blue, amber (`/theme`)
|
|
24
|
-
- **Conversation tools** - `/compact`, `/copy`, `/diff`, `/search`, `/stats`
|
|
24
|
+
- **Conversation tools** - `/compact`, `/copy`, `/diff`, `/search`, `/grep`, `/stats`, `/undo`
|
|
25
|
+
- **Message pinning** - Pin important messages (`/pin`), view pins (`/pins`), export bookmarks (`/bookmark`)
|
|
26
|
+
- **Command aliases** - Create custom shortcuts for frequently used commands (`/alias`)
|
|
27
|
+
- **Code snippets** - Save and load reusable text snippets (`/snippet`)
|
|
28
|
+
- **Debug mode** - Toggle internal state display (`/debug`)
|
|
29
|
+
- **Session context** - View active settings summary (`/context`)
|
|
30
|
+
- **Toast notifications** - Transient status bar messages for save/load/export/theme actions
|
|
25
31
|
- **Hotkey navigation** - Ctrl+D (dashboard), Ctrl+K (palette), Ctrl+S (sessions), Escape (back)
|
|
26
32
|
- **Tab completion** - Type `/` and Tab to auto-complete slash commands
|
|
27
|
-
- **Input history** - Up/down arrow to navigate previous inputs
|
|
28
|
-
- **Progress panel** - Visual progress bars for long operations
|
|
33
|
+
- **Input history** - Up/down arrow to navigate previous inputs, `/history` to view
|
|
34
|
+
- **Progress panel** - Visual progress bars for long operations
|
|
29
35
|
- **Animated spinner** - Status bar spinner during LLM thinking
|
|
30
36
|
- **Daemon routing** - Routes through LegionIO daemon when available, falls back to direct
|
|
31
37
|
- **Second-run flow** - Skips onboarding, re-scans environment, drops into chat
|
|
@@ -74,9 +80,9 @@ legion chat prompt "explain async cognition"
|
|
|
74
80
|
| `/quit` | Exit (auto-saves session) |
|
|
75
81
|
| `/clear` | Clear message history |
|
|
76
82
|
| `/model <name>` | Switch LLM model at runtime |
|
|
77
|
-
| `/session <name>` |
|
|
83
|
+
| `/session <name>` | Set session name |
|
|
78
84
|
| `/cost` | Show token usage and estimated cost |
|
|
79
|
-
| `/export [md\|json]` | Export chat history to file |
|
|
85
|
+
| `/export [md\|json\|html]` | Export chat history to file |
|
|
80
86
|
| `/tools` | List discovered LEX extensions |
|
|
81
87
|
| `/dashboard` | Toggle operational dashboard |
|
|
82
88
|
| `/hotkeys` | Show registered hotkey bindings |
|
|
@@ -91,11 +97,24 @@ legion chat prompt "explain async cognition"
|
|
|
91
97
|
| `/config` | View and edit settings files |
|
|
92
98
|
| `/theme [name]` | Switch color theme (purple/green/blue/amber) |
|
|
93
99
|
| `/search <text>` | Search message history |
|
|
100
|
+
| `/grep <pattern>` | Regex search across messages |
|
|
94
101
|
| `/compact [N]` | Keep last N message pairs, remove older |
|
|
95
102
|
| `/copy` | Copy last assistant response to clipboard |
|
|
96
103
|
| `/diff` | Show new messages since last session load |
|
|
97
104
|
| `/stats` | Show conversation statistics |
|
|
98
105
|
| `/personality [style]` | Switch personality (default/concise/detailed/friendly/technical) |
|
|
106
|
+
| `/undo` | Remove last user+assistant message pair |
|
|
107
|
+
| `/history` | Show input history |
|
|
108
|
+
| `/pin [N]` | Pin a message (last assistant or by index) |
|
|
109
|
+
| `/pins` | Show all pinned messages |
|
|
110
|
+
| `/rename <name>` | Rename current session (moves saved file) |
|
|
111
|
+
| `/context` | Show active session state summary |
|
|
112
|
+
| `/alias [name] [cmd]` | Create or list command aliases |
|
|
113
|
+
| `/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 |
|
|
99
118
|
|
|
100
119
|
## Hotkeys
|
|
101
120
|
|
|
@@ -120,16 +139,22 @@ legion-tty
|
|
|
120
139
|
|
|
121
140
|
Screens/
|
|
122
141
|
Onboarding # First-run wizard (rain -> intro -> wizard -> reveal)
|
|
123
|
-
Chat # AI chat REPL with streaming + slash commands
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
142
|
+
Chat # AI chat REPL with streaming + 40 slash commands
|
|
143
|
+
SessionCommands # save/load/sessions/delete/rename
|
|
144
|
+
ExportCommands # export/bookmark/html/json/markdown
|
|
145
|
+
MessageCommands # compact/copy/diff/search/grep/undo/pin/pins
|
|
146
|
+
UiCommands # help/clear/dashboard/hotkeys/palette/context/stats/debug/history/uptime/time
|
|
147
|
+
ModelCommands # model/system/personality switching
|
|
148
|
+
CustomCommands # alias/snippet management
|
|
149
|
+
Dashboard # Service/LLM status, panel navigation (j/k/1-5)
|
|
150
|
+
Extensions # LEX gem browser by category with homepage opener
|
|
151
|
+
Config # Settings file viewer/editor with JSON validation
|
|
127
152
|
|
|
128
153
|
Components/
|
|
129
154
|
DigitalRain # Matrix-style falling characters
|
|
130
|
-
InputBar # Prompt line with tab completion +
|
|
131
|
-
MessageStream # Scrollable message history with markdown
|
|
132
|
-
StatusBar # Model, plan
|
|
155
|
+
InputBar # Prompt line with tab completion + input history
|
|
156
|
+
MessageStream # Scrollable message history with markdown + timestamps
|
|
157
|
+
StatusBar # Model, plan, debug, notifications, thinking, tokens, cost, session, scroll
|
|
133
158
|
ToolPanel # Expandable tool use panels
|
|
134
159
|
MarkdownView # TTY::Markdown rendering
|
|
135
160
|
WizardPrompt # TTY::Prompt wrappers
|
|
@@ -161,6 +186,7 @@ Identity and credentials are stored in `~/.legionio/settings/`:
|
|
|
161
186
|
- `credentials.json` - LLM provider and API key (chmod 600)
|
|
162
187
|
|
|
163
188
|
Sessions are stored in `~/.legionio/sessions/`.
|
|
189
|
+
Snippets are stored in `~/.legionio/snippets/`.
|
|
164
190
|
Exports go to `~/.legionio/exports/`.
|
|
165
191
|
Boot logs go to `~/.legionio/logs/tty-boot.log`.
|
|
166
192
|
|
|
@@ -168,8 +194,8 @@ Boot logs go to `~/.legionio/logs/tty-boot.log`.
|
|
|
168
194
|
|
|
169
195
|
```bash
|
|
170
196
|
bundle install
|
|
171
|
-
bundle exec rspec #
|
|
172
|
-
bundle exec rubocop #
|
|
197
|
+
bundle exec rspec # 836 examples, 0 failures
|
|
198
|
+
bundle exec rubocop # 92 files, 0 offenses
|
|
173
199
|
```
|
|
174
200
|
|
|
175
201
|
## License
|
|
@@ -6,10 +6,11 @@ require_relative 'notification'
|
|
|
6
6
|
module Legion
|
|
7
7
|
module TTY
|
|
8
8
|
module Components
|
|
9
|
+
# rubocop:disable Metrics/ClassLength
|
|
9
10
|
class StatusBar
|
|
10
11
|
def initialize
|
|
11
12
|
@state = { model: nil, tokens: 0, cost: 0.0, session: 'default', thinking: false, plan_mode: false,
|
|
12
|
-
debug_mode: false }
|
|
13
|
+
debug_mode: false, message_count: 0 }
|
|
13
14
|
@notifications = []
|
|
14
15
|
end
|
|
15
16
|
|
|
@@ -46,6 +47,7 @@ module Legion
|
|
|
46
47
|
notification_segment,
|
|
47
48
|
tokens_segment,
|
|
48
49
|
cost_segment,
|
|
50
|
+
message_count_segment,
|
|
49
51
|
session_segment,
|
|
50
52
|
scroll_segment
|
|
51
53
|
].compact
|
|
@@ -90,6 +92,11 @@ module Legion
|
|
|
90
92
|
Theme.c(:success, format('$%.3f', @state[:cost])) if @state[:cost].to_f.positive?
|
|
91
93
|
end
|
|
92
94
|
|
|
95
|
+
def message_count_segment
|
|
96
|
+
count = @state[:message_count].to_i
|
|
97
|
+
Theme.c(:muted, "#{count} msgs") if count.positive?
|
|
98
|
+
end
|
|
99
|
+
|
|
93
100
|
def session_segment
|
|
94
101
|
Theme.c(:muted, @state[:session]) if @state[:session]
|
|
95
102
|
end
|
|
@@ -132,6 +139,7 @@ module Legion
|
|
|
132
139
|
result
|
|
133
140
|
end
|
|
134
141
|
end
|
|
142
|
+
# rubocop:enable Metrics/ClassLength
|
|
135
143
|
end
|
|
136
144
|
end
|
|
137
145
|
end
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module TTY
|
|
5
|
+
module Screens
|
|
6
|
+
class Chat < Base
|
|
7
|
+
# rubocop:disable Metrics/ModuleLength
|
|
8
|
+
module CustomCommands
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def handle_alias(input)
|
|
12
|
+
parts = input.split(nil, 3)
|
|
13
|
+
if parts.size < 2
|
|
14
|
+
if @aliases.empty?
|
|
15
|
+
@message_stream.add_message(role: :system, content: 'No aliases defined.')
|
|
16
|
+
else
|
|
17
|
+
lines = @aliases.map { |k, v| " #{k} => #{v}" }
|
|
18
|
+
@message_stream.add_message(role: :system, content: "Aliases:\n#{lines.join("\n")}")
|
|
19
|
+
end
|
|
20
|
+
return :handled
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
shortname = parts[1]
|
|
24
|
+
expansion = parts[2]
|
|
25
|
+
unless expansion
|
|
26
|
+
@message_stream.add_message(role: :system, content: 'Usage: /alias <shortname> <command and args>')
|
|
27
|
+
return :handled
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
alias_key = shortname.start_with?('/') ? shortname : "/#{shortname}"
|
|
31
|
+
@aliases[alias_key] = expansion
|
|
32
|
+
@message_stream.add_message(role: :system, content: "Alias created: #{alias_key} => #{expansion}")
|
|
33
|
+
:handled
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def handle_snippet(input)
|
|
37
|
+
parts = input.split(nil, 3)
|
|
38
|
+
subcommand = parts[1]
|
|
39
|
+
name = parts[2]
|
|
40
|
+
|
|
41
|
+
case subcommand
|
|
42
|
+
when 'save'
|
|
43
|
+
snippet_save(name)
|
|
44
|
+
when 'load'
|
|
45
|
+
snippet_load(name)
|
|
46
|
+
when 'list'
|
|
47
|
+
snippet_list
|
|
48
|
+
when 'delete'
|
|
49
|
+
snippet_delete(name)
|
|
50
|
+
else
|
|
51
|
+
@message_stream.add_message(
|
|
52
|
+
role: :system,
|
|
53
|
+
content: 'Usage: /snippet save|load|list|delete <name>'
|
|
54
|
+
)
|
|
55
|
+
end
|
|
56
|
+
:handled
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def snippet_dir
|
|
60
|
+
File.expand_path('~/.legionio/snippets')
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# rubocop:disable Metrics/AbcSize
|
|
64
|
+
def snippet_save(name)
|
|
65
|
+
unless name
|
|
66
|
+
@message_stream.add_message(role: :system, content: 'Usage: /snippet save <name>')
|
|
67
|
+
return
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
last_assistant = @message_stream.messages.reverse.find { |m| m[:role] == :assistant }
|
|
71
|
+
unless last_assistant
|
|
72
|
+
@message_stream.add_message(role: :system, content: 'No assistant message to save as snippet.')
|
|
73
|
+
return
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
require 'fileutils'
|
|
77
|
+
FileUtils.mkdir_p(snippet_dir)
|
|
78
|
+
path = File.join(snippet_dir, "#{name}.txt")
|
|
79
|
+
File.write(path, last_assistant[:content].to_s)
|
|
80
|
+
@snippets[name] = last_assistant[:content].to_s
|
|
81
|
+
@message_stream.add_message(role: :system, content: "Snippet '#{name}' saved.")
|
|
82
|
+
end
|
|
83
|
+
# rubocop:enable Metrics/AbcSize
|
|
84
|
+
|
|
85
|
+
def snippet_load(name)
|
|
86
|
+
unless name
|
|
87
|
+
@message_stream.add_message(role: :system, content: 'Usage: /snippet load <name>')
|
|
88
|
+
return
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
content = @snippets[name]
|
|
92
|
+
if content.nil?
|
|
93
|
+
path = File.join(snippet_dir, "#{name}.txt")
|
|
94
|
+
content = File.read(path) if File.exist?(path)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
unless content
|
|
98
|
+
@message_stream.add_message(role: :system, content: "Snippet '#{name}' not found.")
|
|
99
|
+
return
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
@snippets[name] = content
|
|
103
|
+
@message_stream.add_message(role: :user, content: content)
|
|
104
|
+
@message_stream.add_message(role: :system, content: "Snippet '#{name}' inserted.")
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# rubocop:disable Metrics/AbcSize
|
|
108
|
+
def snippet_list
|
|
109
|
+
disk_snippets = Dir.glob(File.join(snippet_dir, '*.txt')).map { |f| File.basename(f, '.txt') }
|
|
110
|
+
all_names = (@snippets.keys + disk_snippets).uniq.sort
|
|
111
|
+
|
|
112
|
+
if all_names.empty?
|
|
113
|
+
@message_stream.add_message(role: :system, content: 'No snippets saved.')
|
|
114
|
+
return
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
lines = all_names.map do |sname|
|
|
118
|
+
content = @snippets[sname] || begin
|
|
119
|
+
path = File.join(snippet_dir, "#{sname}.txt")
|
|
120
|
+
File.exist?(path) ? File.read(path) : ''
|
|
121
|
+
end
|
|
122
|
+
" #{sname}: #{truncate_text(content.to_s, 60)}"
|
|
123
|
+
end
|
|
124
|
+
@message_stream.add_message(role: :system, content: "Snippets (#{all_names.size}):\n#{lines.join("\n")}")
|
|
125
|
+
end
|
|
126
|
+
# rubocop:enable Metrics/AbcSize
|
|
127
|
+
|
|
128
|
+
def snippet_delete(name)
|
|
129
|
+
unless name
|
|
130
|
+
@message_stream.add_message(role: :system, content: 'Usage: /snippet delete <name>')
|
|
131
|
+
return
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
@snippets.delete(name)
|
|
135
|
+
path = File.join(snippet_dir, "#{name}.txt")
|
|
136
|
+
if File.exist?(path)
|
|
137
|
+
File.delete(path)
|
|
138
|
+
@message_stream.add_message(role: :system, content: "Snippet '#{name}' deleted.")
|
|
139
|
+
else
|
|
140
|
+
@message_stream.add_message(role: :system, content: "Snippet '#{name}' not found.")
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
# rubocop:enable Metrics/ModuleLength
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module TTY
|
|
5
|
+
module Screens
|
|
6
|
+
class Chat < Base
|
|
7
|
+
module ExportCommands
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def handle_export(input)
|
|
11
|
+
require 'fileutils'
|
|
12
|
+
path = build_export_path(input)
|
|
13
|
+
dispatch_export(path, input.split[1]&.downcase)
|
|
14
|
+
@status_bar.notify(message: 'Exported', level: :success, ttl: 3)
|
|
15
|
+
@message_stream.add_message(role: :system, content: "Exported to: #{path}")
|
|
16
|
+
:handled
|
|
17
|
+
rescue StandardError => e
|
|
18
|
+
@message_stream.add_message(role: :system, content: "Export failed: #{e.message}")
|
|
19
|
+
:handled
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def build_export_path(input)
|
|
23
|
+
format = input.split[1]&.downcase
|
|
24
|
+
format = 'md' unless %w[json md html].include?(format)
|
|
25
|
+
exports_dir = File.expand_path('~/.legionio/exports')
|
|
26
|
+
FileUtils.mkdir_p(exports_dir)
|
|
27
|
+
timestamp = Time.now.strftime('%Y%m%d-%H%M%S')
|
|
28
|
+
ext = { 'json' => 'json', 'md' => 'md', 'html' => 'html' }[format]
|
|
29
|
+
File.join(exports_dir, "chat-#{timestamp}.#{ext}")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def dispatch_export(path, format)
|
|
33
|
+
if format == 'json'
|
|
34
|
+
export_json(path)
|
|
35
|
+
elsif format == 'html'
|
|
36
|
+
export_html(path)
|
|
37
|
+
else
|
|
38
|
+
export_markdown(path)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def export_markdown(path)
|
|
43
|
+
lines = ["# Chat Export\n", "_Exported: #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}_\n\n---\n"]
|
|
44
|
+
@message_stream.messages.each do |msg|
|
|
45
|
+
role_label = msg[:role].to_s.capitalize
|
|
46
|
+
lines << "\n**#{role_label}**\n\n#{msg[:content]}\n"
|
|
47
|
+
end
|
|
48
|
+
File.write(path, lines.join)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def export_json(path)
|
|
52
|
+
require 'json'
|
|
53
|
+
data = {
|
|
54
|
+
exported_at: Time.now.iso8601,
|
|
55
|
+
token_summary: @token_tracker.summary,
|
|
56
|
+
messages: @message_stream.messages.map { |m| { role: m[:role].to_s, content: m[:content] } }
|
|
57
|
+
}
|
|
58
|
+
File.write(path, ::JSON.pretty_generate(data))
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
|
62
|
+
def export_html(path)
|
|
63
|
+
lines = [
|
|
64
|
+
'<!DOCTYPE html><html><head>',
|
|
65
|
+
'<meta charset="utf-8">',
|
|
66
|
+
'<title>Chat Export</title>',
|
|
67
|
+
'<style>',
|
|
68
|
+
'body { font-family: system-ui; max-width: 800px; margin: 0 auto; ' \
|
|
69
|
+
'padding: 20px; background: #1e1b2e; color: #d0cce6; }',
|
|
70
|
+
'.msg { margin: 12px 0; padding: 8px 12px; border-radius: 8px; }',
|
|
71
|
+
'.user { background: #2a2640; }',
|
|
72
|
+
'.assistant { background: #1a1730; }',
|
|
73
|
+
'.system { background: #25223a; color: #8b85a8; font-style: italic; }',
|
|
74
|
+
'.role { font-weight: bold; color: #9d91e6; font-size: 0.85em; }',
|
|
75
|
+
'</style></head><body>',
|
|
76
|
+
'<h1>Chat Export</h1>',
|
|
77
|
+
"<p>Exported: #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}</p>"
|
|
78
|
+
]
|
|
79
|
+
@message_stream.messages.each do |msg|
|
|
80
|
+
role = msg[:role].to_s
|
|
81
|
+
content = escape_html(msg[:content].to_s).gsub("\n", '<br>')
|
|
82
|
+
lines << "<div class='msg #{role}'>"
|
|
83
|
+
lines << "<span class='role'>#{role.capitalize}</span>"
|
|
84
|
+
lines << "<p>#{content}</p>"
|
|
85
|
+
lines << '</div>'
|
|
86
|
+
end
|
|
87
|
+
lines << '</body></html>'
|
|
88
|
+
File.write(path, lines.join("\n"))
|
|
89
|
+
end
|
|
90
|
+
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize
|
|
91
|
+
|
|
92
|
+
def escape_html(text)
|
|
93
|
+
text.gsub('&', '&').gsub('<', '<').gsub('>', '>').gsub('"', '"')
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# rubocop:disable Metrics/AbcSize
|
|
97
|
+
def handle_bookmark
|
|
98
|
+
require 'fileutils'
|
|
99
|
+
if @pinned_messages.empty?
|
|
100
|
+
@message_stream.add_message(role: :system, content: 'No pinned messages to export.')
|
|
101
|
+
return :handled
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
exports_dir = File.expand_path('~/.legionio/exports')
|
|
105
|
+
FileUtils.mkdir_p(exports_dir)
|
|
106
|
+
timestamp = Time.now.strftime('%Y%m%d-%H%M%S')
|
|
107
|
+
path = File.join(exports_dir, "bookmarks-#{timestamp}.md")
|
|
108
|
+
lines = ["# Pinned Messages\n", "_Exported: #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}_\n\n---\n"]
|
|
109
|
+
@pinned_messages.each_with_index do |msg, i|
|
|
110
|
+
role_label = msg[:role].to_s.capitalize
|
|
111
|
+
lines << "\n## Bookmark #{i + 1} (#{role_label})\n\n#{msg[:content]}\n"
|
|
112
|
+
end
|
|
113
|
+
File.write(path, lines.join)
|
|
114
|
+
@message_stream.add_message(role: :system, content: "Bookmarks exported to: #{path}")
|
|
115
|
+
:handled
|
|
116
|
+
rescue StandardError => e
|
|
117
|
+
@message_stream.add_message(role: :system, content: "Bookmark export failed: #{e.message}")
|
|
118
|
+
:handled
|
|
119
|
+
end
|
|
120
|
+
# rubocop:enable Metrics/AbcSize
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|