legion-tty 0.4.14 → 0.4.16

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: bce8d6f696692fc8e9173eee8cb0afd87f37dc48b12325a149a7de1748f13447
4
- data.tar.gz: d0e115df860ddd59c02e92b27d65a74a3a1413bafd25c7484f8dcc8c631832fb
3
+ metadata.gz: 70299176c70666c5c4a4e7a1e6ac8ae1a03050525e299b7fcc4cf6a4cb8146c4
4
+ data.tar.gz: c3cf40f5acfa8e14da112380c93931340985d937aafd6667c638bdb4b3ed0913
5
5
  SHA512:
6
- metadata.gz: c6bc9b9eb07cf1838830fddd0bf626c4d9a0404db3330f7f6a658c5dd6425f9293d316a3a6f013858262f47ddb2875421c768a849a14213432028d66f56d94c7
7
- data.tar.gz: 4eddcc425c258ec26d3c74e9ec3a9ce1ac470f8b25e985bed26db41f504e3825a61f21113587ccb70c5e7ce35789e06325affba7b704e0580687165687d4dbf5
6
+ metadata.gz: 8df88d8c600985633a63d34c18c3d1366dd9868d4109c2240c7522fa905511d04d865a8a259af71009840571ec627dae22f0def660043ecbc63701a4a0564dcb
7
+ data.tar.gz: 7b607c3304efb10d94866d23eabd122b1aee84e0cc6cb8ecd7329129ad6b18277867b4e74825788d0bbc171563ea6a60c73d75fd2bdeeb7be1e9227ec26515a0
data/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.16] - 2026-03-19
4
+
5
+ ### Added
6
+ - Extensions screen category filter: 'f' cycles through Core/AI/Service/Agentic/Other, 'c' clears
7
+ - Config screen backup: 'b' creates .bak copy, auto-backup before edits
8
+ - `/repeat` command: re-execute the last slash command
9
+ - `/count <pattern>` command: count messages matching a pattern with per-role breakdown
10
+
11
+ ## [0.4.15] - 2026-03-19
12
+
13
+ ### Added
14
+ - `/autosave [N|off]` command: toggle periodic auto-save with configurable interval (default 60s)
15
+ - `/react <emoji>` command: add emoji reactions to messages (displayed in render)
16
+ - `/macro record|stop|play|list|delete` command: record and replay slash command sequences
17
+ - `/tag` and `/tags` commands: tag messages with labels, filter by tag, show tag statistics
18
+
3
19
  ## [0.4.14] - 2026-03-19
4
20
 
5
21
  ### Added
@@ -96,7 +96,9 @@ module Legion
96
96
  def user_lines(msg, _width)
97
97
  ts = format_timestamp(msg[:timestamp])
98
98
  header = "#{Theme.c(:accent, 'You')} #{Theme.c(:muted, ts)}"
99
- ['', "#{header}: #{msg[:content]}"]
99
+ lines = ['', "#{header}: #{msg[:content]}"]
100
+ lines << reaction_line(msg) if msg[:reactions]&.any?
101
+ lines
100
102
  end
101
103
 
102
104
  def format_timestamp(time)
@@ -107,7 +109,14 @@ module Legion
107
109
 
108
110
  def assistant_lines(msg, width)
109
111
  rendered = render_markdown(msg[:content], width)
110
- ['', *rendered.split("\n")]
112
+ lines = ['', *rendered.split("\n")]
113
+ lines << reaction_line(msg) if msg[:reactions]&.any?
114
+ lines
115
+ end
116
+
117
+ def reaction_line(msg)
118
+ reactions = msg[:reactions].map { |r| "[#{r}]" }.join(' ')
119
+ " #{Theme.c(:muted, reactions)}"
111
120
  end
112
121
 
113
122
  def render_markdown(text, width)
@@ -125,6 +125,105 @@ module Legion
125
125
  end
126
126
  # rubocop:enable Metrics/AbcSize
127
127
 
128
+ # rubocop:disable Metrics/MethodLength
129
+ def handle_macro(input)
130
+ parts = input.split(nil, 3)
131
+ subcommand = parts[1]
132
+ name = parts[2]
133
+
134
+ case subcommand
135
+ when 'record'
136
+ macro_record(name)
137
+ when 'stop'
138
+ macro_stop
139
+ when 'play'
140
+ macro_play(name)
141
+ when 'list'
142
+ macro_list
143
+ when 'delete'
144
+ macro_delete(name)
145
+ else
146
+ @message_stream.add_message(
147
+ role: :system,
148
+ content: 'Usage: /macro record|stop|play|list|delete <name>'
149
+ )
150
+ end
151
+ :handled
152
+ end
153
+ # rubocop:enable Metrics/MethodLength
154
+
155
+ def macro_record(name)
156
+ unless name
157
+ @message_stream.add_message(role: :system, content: 'Usage: /macro record <name>')
158
+ return
159
+ end
160
+
161
+ @recording_macro = name
162
+ @macro_buffer = []
163
+ @message_stream.add_message(role: :system,
164
+ content: "Recording macro '#{name}'... Use /macro stop to finish.")
165
+ end
166
+
167
+ def macro_stop
168
+ unless @recording_macro
169
+ @message_stream.add_message(role: :system, content: 'No macro recording in progress.')
170
+ return
171
+ end
172
+
173
+ name = @recording_macro
174
+ @macros[name] = @macro_buffer.dup
175
+ @recording_macro = nil
176
+ @macro_buffer = []
177
+ @message_stream.add_message(role: :system,
178
+ content: "Macro '#{name}' saved (#{@macros[name].size} commands).")
179
+ end
180
+
181
+ def macro_play(name)
182
+ unless name
183
+ @message_stream.add_message(role: :system, content: 'Usage: /macro play <name>')
184
+ return
185
+ end
186
+
187
+ commands = @macros[name]
188
+ unless commands
189
+ @message_stream.add_message(role: :system, content: "Macro '#{name}' not found.")
190
+ return
191
+ end
192
+
193
+ @message_stream.add_message(role: :system,
194
+ content: "Playing macro '#{name}' (#{commands.size} commands)...")
195
+ commands.each { |cmd| handle_slash_command(cmd) }
196
+ end
197
+
198
+ def macro_list
199
+ if @macros.empty?
200
+ @message_stream.add_message(role: :system, content: 'No macros defined.')
201
+ return
202
+ end
203
+
204
+ lines = @macros.map do |n, cmds|
205
+ preview = cmds.first(3).join(', ')
206
+ preview += ', ...' if cmds.size > 3
207
+ " #{n} (#{cmds.size}): #{preview}"
208
+ end
209
+ status = @recording_macro ? " [recording: #{@recording_macro}]" : ''
210
+ @message_stream.add_message(role: :system,
211
+ content: "Macros (#{@macros.size})#{status}:\n#{lines.join("\n")}")
212
+ end
213
+
214
+ def macro_delete(name)
215
+ unless name
216
+ @message_stream.add_message(role: :system, content: 'Usage: /macro delete <name>')
217
+ return
218
+ end
219
+
220
+ if @macros.delete(name)
221
+ @message_stream.add_message(role: :system, content: "Macro '#{name}' deleted.")
222
+ else
223
+ @message_stream.add_message(role: :system, content: "Macro '#{name}' not found.")
224
+ end
225
+ end
226
+
128
227
  def snippet_delete(name)
129
228
  unless name
130
229
  @message_stream.add_message(role: :system, content: 'Usage: /snippet delete <name>')
@@ -162,6 +162,92 @@ module Legion
162
162
  :handled
163
163
  end
164
164
 
165
+ # rubocop:disable Metrics/AbcSize
166
+ def handle_react(input)
167
+ parts = input.split(nil, 3)
168
+ if parts.size == 2
169
+ emoji = parts[1]
170
+ msg = @message_stream.messages.reverse.find { |m| m[:role] == :assistant }
171
+ elsif parts.size >= 3 && parts[1].match?(/\A\d+\z/)
172
+ idx = parts[1].to_i
173
+ emoji = parts[2]
174
+ msg = @message_stream.messages[idx]
175
+ else
176
+ @message_stream.add_message(role: :system, content: 'Usage: /react <emoji> or /react <N> <emoji>')
177
+ return :handled
178
+ end
179
+
180
+ unless msg
181
+ @message_stream.add_message(role: :system, content: 'No message to react to.')
182
+ return :handled
183
+ end
184
+
185
+ msg[:reactions] ||= []
186
+ msg[:reactions] << emoji
187
+ @message_stream.add_message(role: :system, content: "Reaction #{emoji} added.")
188
+ :handled
189
+ end
190
+ # rubocop:enable Metrics/AbcSize
191
+
192
+ # rubocop:disable Metrics/AbcSize
193
+ def handle_tag(input)
194
+ parts = input.split(nil, 3)
195
+ if parts.size == 2
196
+ label = parts[1]
197
+ msg = @message_stream.messages.reverse.find { |m| m[:role] == :assistant }
198
+ elsif parts.size >= 3 && parts[1].match?(/\A\d+\z/)
199
+ idx = parts[1].to_i
200
+ label = parts[2]
201
+ msg = @message_stream.messages[idx]
202
+ else
203
+ @message_stream.add_message(role: :system, content: 'Usage: /tag <label> or /tag <N> <label>')
204
+ return :handled
205
+ end
206
+
207
+ unless msg
208
+ @message_stream.add_message(role: :system, content: 'No message to tag.')
209
+ return :handled
210
+ end
211
+
212
+ msg[:tags] ||= []
213
+ msg[:tags] |= [label]
214
+ @message_stream.add_message(role: :system, content: "Tag '#{label}' added.")
215
+ :handled
216
+ end
217
+ # rubocop:enable Metrics/AbcSize
218
+
219
+ def handle_tags(input)
220
+ label = input.split(nil, 2)[1]
221
+ if label
222
+ filter_messages_by_tag(label)
223
+ else
224
+ show_all_tags
225
+ end
226
+ :handled
227
+ end
228
+
229
+ def handle_count(input)
230
+ query = input.split(nil, 2)[1]
231
+ unless query
232
+ @message_stream.add_message(role: :system, content: 'Usage: /count <pattern>')
233
+ return :handled
234
+ end
235
+
236
+ results = search_messages(query)
237
+ if results.empty?
238
+ @message_stream.add_message(role: :system, content: "0 messages matching '#{query}'.")
239
+ else
240
+ breakdown = results.group_by { |m| m[:role] }
241
+ .map { |role, msgs| "#{role}: #{msgs.size}" }
242
+ .join(', ')
243
+ @message_stream.add_message(
244
+ role: :system,
245
+ content: "#{results.size} message(s) matching '#{query}' (#{breakdown})."
246
+ )
247
+ end
248
+ :handled
249
+ end
250
+
165
251
  def search_messages(query)
166
252
  pattern = query.downcase
167
253
  @message_stream.messages.select do |msg|
@@ -184,6 +270,34 @@ module Legion
184
270
  nil
185
271
  end
186
272
  end
273
+
274
+ # rubocop:disable Metrics/AbcSize
275
+ def show_all_tags
276
+ tagged = @message_stream.messages.select { |m| m[:tags]&.any? }
277
+ if tagged.empty?
278
+ @message_stream.add_message(role: :system, content: 'No tagged messages.')
279
+ return
280
+ end
281
+
282
+ counts = Hash.new(0)
283
+ tagged.each { |m| m[:tags].each { |t| counts[t] += 1 } }
284
+ lines = counts.sort.map { |tag, count| " ##{tag} (#{count})" }
285
+ @message_stream.add_message(role: :system, content: "Tags:\n#{lines.join("\n")}")
286
+ end
287
+ # rubocop:enable Metrics/AbcSize
288
+
289
+ def filter_messages_by_tag(label)
290
+ results = @message_stream.messages.select { |m| m[:tags]&.include?(label) }
291
+ if results.empty?
292
+ @message_stream.add_message(role: :system, content: "No messages tagged '##{label}'.")
293
+ else
294
+ lines = results.map { |r| " [#{r[:role]}] #{truncate_text(r[:content].to_s, 80)}" }
295
+ @message_stream.add_message(
296
+ role: :system,
297
+ content: "Messages tagged '##{label}' (#{results.size}):\n#{lines.join("\n")}"
298
+ )
299
+ end
300
+ end
187
301
  end
188
302
  # rubocop:enable Metrics/ModuleLength
189
303
  end
@@ -113,6 +113,39 @@ module Legion
113
113
  :handled
114
114
  end
115
115
 
116
+ def handle_autosave(input)
117
+ arg = input.split(nil, 2)[1]
118
+ if arg.nil?
119
+ @autosave_enabled = !@autosave_enabled
120
+ status = @autosave_enabled ? "ON (every #{@autosave_interval}s)" : 'OFF'
121
+ @status_bar.notify(message: "Autosave: #{status}", level: :info, ttl: 3)
122
+ @message_stream.add_message(role: :system, content: "Autosave #{status}.")
123
+ elsif arg == 'off'
124
+ @autosave_enabled = false
125
+ @status_bar.notify(message: 'Autosave: OFF', level: :info, ttl: 3)
126
+ @message_stream.add_message(role: :system, content: 'Autosave OFF.')
127
+ elsif arg.match?(/\A\d+\z/)
128
+ @autosave_interval = arg.to_i
129
+ @autosave_enabled = true
130
+ @status_bar.notify(message: "Autosave: ON (every #{@autosave_interval}s)", level: :info, ttl: 3)
131
+ @message_stream.add_message(role: :system, content: "Autosave ON (every #{@autosave_interval}s).")
132
+ else
133
+ @message_stream.add_message(role: :system, content: 'Usage: /autosave [off|<seconds>]')
134
+ end
135
+ :handled
136
+ end
137
+
138
+ def check_autosave
139
+ return unless @autosave_enabled
140
+ return unless Time.now - @last_autosave >= @autosave_interval
141
+
142
+ auto_save_session
143
+ @last_autosave = Time.now
144
+ @status_bar.notify(message: 'Autosaved', level: :info, ttl: 2)
145
+ rescue StandardError
146
+ nil
147
+ end
148
+
116
149
  def auto_save_session
117
150
  return if @message_stream.messages.empty?
118
151
 
@@ -29,7 +29,7 @@ module Legion
29
29
  /hotkeys /save /load /sessions /system /delete /plan /palette /extensions /config
30
30
  /theme /search /grep /stats /personality /undo /history /pin /pins /rename
31
31
  /context /alias /snippet /debug /uptime /time /bookmark /welcome /tips
32
- /wc /import /mute].freeze
32
+ /wc /import /mute /autosave /react /macro /tag /tags /repeat /count].freeze
33
33
 
34
34
  PERSONALITIES = {
35
35
  'default' => 'You are Legion, an async cognition engine and AI assistant. Be helpful and concise.',
@@ -41,7 +41,7 @@ module Legion
41
41
 
42
42
  attr_reader :message_stream, :status_bar
43
43
 
44
- # rubocop:disable Metrics/AbcSize
44
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
45
45
  def initialize(app, output: $stdout, input_bar: nil)
46
46
  super(app)
47
47
  @output = output
@@ -57,12 +57,19 @@ module Legion
57
57
  @pinned_messages = []
58
58
  @aliases = {}
59
59
  @snippets = {}
60
+ @macros = {}
60
61
  @debug_mode = false
61
62
  @session_start = Time.now
62
63
  @muted_system = false
64
+ @autosave_enabled = false
65
+ @autosave_interval = 60
66
+ @last_autosave = Time.now
67
+ @recording_macro = nil
68
+ @macro_buffer = []
69
+ @last_command = nil
63
70
  end
64
71
 
65
- # rubocop:enable Metrics/AbcSize
72
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
66
73
 
67
74
  def activate
68
75
  @running = true
@@ -116,7 +123,10 @@ module Legion
116
123
  return handle_slash_command("#{expanded} #{input.split(nil, 2)[1]}".strip)
117
124
  end
118
125
 
119
- dispatch_slash(cmd, input)
126
+ result = dispatch_slash(cmd, input)
127
+ @last_command = input if cmd != '/repeat'
128
+ record_macro_step(input, cmd, result)
129
+ result
120
130
  end
121
131
 
122
132
  def handle_user_message(input)
@@ -128,6 +138,7 @@ module Legion
128
138
  send_to_llm(input)
129
139
  end
130
140
  @status_bar.update(message_count: @message_stream.messages.size)
141
+ check_autosave
131
142
  render_screen
132
143
  end
133
144
 
@@ -175,6 +186,14 @@ module Legion
175
186
 
176
187
  private
177
188
 
189
+ def record_macro_step(input, cmd, result)
190
+ return unless @recording_macro
191
+ return if cmd == '/macro'
192
+ return unless result == :handled
193
+
194
+ @macro_buffer << input
195
+ end
196
+
178
197
  def setup_system_prompt
179
198
  cfg = safe_config
180
199
  return unless @llm_chat && cfg.is_a?(Hash) && !cfg.empty?
@@ -353,11 +372,27 @@ module Legion
353
372
  when '/wc' then handle_wc
354
373
  when '/import' then handle_import(input)
355
374
  when '/mute' then handle_mute
375
+ when '/autosave' then handle_autosave(input)
376
+ when '/react' then handle_react(input)
377
+ when '/macro' then handle_macro(input)
378
+ when '/tag' then handle_tag(input)
379
+ when '/tags' then handle_tags(input)
380
+ when '/repeat' then handle_repeat
381
+ when '/count' then handle_count(input)
356
382
  else :handled
357
383
  end
358
384
  end
359
385
  # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
360
386
 
387
+ def handle_repeat
388
+ unless @last_command
389
+ @message_stream.add_message(role: :system, content: 'No previous command to repeat.')
390
+ return :handled
391
+ end
392
+
393
+ dispatch_slash(@last_command.split.first, @last_command)
394
+ end
395
+
361
396
  def handle_cost
362
397
  @message_stream.add_message(role: :system, content: @token_tracker.summary)
363
398
  :handled
@@ -431,7 +466,9 @@ module Legion
431
466
  "personality:#{@personality || 'default'} " \
432
467
  "aliases:#{@aliases.size} " \
433
468
  "snippets:#{@snippets.size} " \
434
- "pinned:#{@pinned_messages.size}"
469
+ "macros:#{@macros.size} " \
470
+ "pinned:#{@pinned_messages.size} " \
471
+ "autosave:#{@autosave_enabled}"
435
472
  end
436
473
 
437
474
  def build_default_input_bar
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'fileutils'
3
4
  require 'json'
4
5
  require_relative 'base'
5
6
  require_relative '../theme'
@@ -41,7 +42,8 @@ module Legion
41
42
  else
42
43
  file_list_lines(height - 4)
43
44
  end
44
- lines += ['', Theme.c(:muted, ' Enter=view e=edit q=back')]
45
+ hint = @viewing_file ? ' Enter=edit b=backup q=back' : ' Enter=view e=edit q=back'
46
+ lines += ['', Theme.c(:muted, hint)]
45
47
  pad_lines(lines, height)
46
48
  end
47
49
 
@@ -79,6 +81,9 @@ module Legion
79
81
  :handled
80
82
  when 'e', :enter then edit_selected_key
81
83
  :handled
84
+ when 'b'
85
+ backup_current_file
86
+ :handled
82
87
  when 'q', :escape
83
88
  @viewing_file = false
84
89
  @selected_key = 0
@@ -130,10 +135,25 @@ module Legion
130
135
  false
131
136
  end
132
137
 
138
+ def backup_config(path)
139
+ return unless File.exist?(path)
140
+
141
+ FileUtils.cp(path, "#{path}.bak")
142
+ end
143
+
144
+ def backup_current_file
145
+ return unless @files[@selected_file]
146
+
147
+ path = @files[@selected_file][:path]
148
+ backup_config(path)
149
+ @backup_notice = "Backed up to #{File.basename(path)}.bak"
150
+ end
151
+
133
152
  def save_current_file
134
153
  return unless @files[@selected_file]
135
154
 
136
155
  path = @files[@selected_file][:path]
156
+ backup_config(path)
137
157
  File.write(path, ::JSON.pretty_generate(@file_data))
138
158
  end
139
159
 
@@ -17,12 +17,15 @@ module Legion
17
17
  SERVICE = %w[lex-http lex-vault lex-github lex-consul lex-kerberos lex-tfe
18
18
  lex-redis lex-memcached lex-elasticsearch lex-s3].freeze
19
19
 
20
+ CATEGORIES = [nil, 'Core', 'AI', 'Service', 'Agentic', 'Other'].freeze
21
+
20
22
  def initialize(app, output: $stdout)
21
23
  super(app)
22
24
  @output = output
23
25
  @gems = []
24
26
  @selected = 0
25
27
  @detail = false
28
+ @filter = nil
26
29
  end
27
30
 
28
31
  def activate
@@ -36,45 +39,64 @@ module Legion
36
39
  end
37
40
 
38
41
  def render(_width, height)
39
- lines = [Theme.c(:accent, ' LEX Extensions'), '']
40
- lines += if @detail && @gems[@selected]
41
- detail_lines(@gems[@selected])
42
+ filter_label = @filter ? Theme.c(:warning, " filter: #{@filter}") : ''
43
+ header = [Theme.c(:accent, ' LEX Extensions'), filter_label].reject(&:empty?)
44
+ lines = header + ['']
45
+ lines += if @detail && current_gems[@selected]
46
+ detail_lines(current_gems[@selected])
42
47
  else
43
48
  list_lines(height - 4)
44
49
  end
45
- lines += ['', Theme.c(:muted, ' Enter=detail o=open q=back')]
50
+ lines += ['', Theme.c(:muted, ' Enter=detail o=open f=filter c=clear q=back')]
46
51
  pad_lines(lines, height)
47
52
  end
48
53
 
49
- # rubocop:disable Metrics/MethodLength
50
54
  def handle_input(key)
51
55
  case key
52
56
  when :up
53
57
  @selected = [(@selected - 1), 0].max
54
58
  :handled
55
59
  when :down
56
- @selected = [(@selected + 1), @gems.size - 1].min
60
+ max = [current_gems.size - 1, 0].max
61
+ @selected = [(@selected + 1), max].min
57
62
  :handled
58
63
  when :enter
59
64
  @detail = !@detail
60
65
  :handled
66
+ when 'q', :escape
67
+ handle_back_key
68
+ else
69
+ handle_action_key(key)
70
+ end
71
+ end
72
+
73
+ private
74
+
75
+ def handle_back_key
76
+ if @detail
77
+ @detail = false
78
+ :handled
79
+ else
80
+ :pop_screen
81
+ end
82
+ end
83
+
84
+ def handle_action_key(key)
85
+ case key
61
86
  when 'o'
62
87
  open_homepage
63
88
  :handled
64
- when 'q', :escape
65
- if @detail
66
- @detail = false
67
- :handled
68
- else
69
- :pop_screen
70
- end
89
+ when 'f'
90
+ cycle_filter
91
+ :handled
92
+ when 'c'
93
+ @filter = nil
94
+ @selected = 0
95
+ :handled
71
96
  else
72
97
  :pass
73
98
  end
74
99
  end
75
- # rubocop:enable Metrics/MethodLength
76
-
77
- private
78
100
 
79
101
  def build_entry(spec)
80
102
  loaded = $LOADED_FEATURES.any? { |f| f.include?(spec.name.tr('-', '/')) }
@@ -100,7 +122,7 @@ module Legion
100
122
 
101
123
  # rubocop:disable Metrics/AbcSize
102
124
  def list_lines(max_height)
103
- grouped = @gems.group_by { |g| g[:category] }
125
+ grouped = current_gems.group_by { |g| g[:category] }
104
126
  lines = []
105
127
  idx = 0
106
128
  grouped.each do |cat, gems|
@@ -132,6 +154,18 @@ module Legion
132
154
  ]
133
155
  end
134
156
 
157
+ def cycle_filter
158
+ idx = CATEGORIES.index(@filter) || 0
159
+ @filter = CATEGORIES[(idx + 1) % CATEGORIES.size]
160
+ @selected = 0
161
+ end
162
+
163
+ def current_gems
164
+ return @gems unless @filter
165
+
166
+ @gems.select { |g| g[:category] == @filter }
167
+ end
168
+
135
169
  def open_homepage
136
170
  entry = current_gem
137
171
  return unless entry && entry[:homepage]
@@ -150,9 +184,10 @@ module Legion
150
184
  end
151
185
 
152
186
  def current_gem
153
- return nil if @gems.empty?
187
+ gems = current_gems
188
+ return nil if gems.empty?
154
189
 
155
- @gems[@selected]
190
+ gems[@selected]
156
191
  end
157
192
 
158
193
  def pad_lines(lines, height)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module TTY
5
- VERSION = '0.4.14'
5
+ VERSION = '0.4.16'
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.14
4
+ version: 0.4.16
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity