kward 0.69.1 → 0.71.0
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/.github/workflows/pages.yml +1 -1
- data/CHANGELOG.md +68 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +90 -2
- data/README.md +30 -6
- data/Rakefile +96 -0
- data/doc/agent-tools.md +43 -0
- data/doc/api.md +92 -0
- data/doc/authentication.md +39 -25
- data/doc/configuration.md +2 -16
- data/doc/context-tools.md +70 -0
- data/doc/getting-started.md +3 -1
- data/doc/plugins.md +2 -2
- data/doc/releasing.md +14 -5
- data/doc/rpc.md +3 -11
- data/doc/session-management.md +220 -0
- data/doc/usage.md +13 -7
- data/doc/workspace-tools.md +105 -0
- data/lib/kward/cli/commands.rb +8 -0
- data/lib/kward/cli/openrouter_commands.rb +55 -0
- data/lib/kward/cli/prompt_interface.rb +85 -7
- data/lib/kward/cli/rendering.rb +11 -6
- data/lib/kward/cli/sessions.rb +454 -15
- data/lib/kward/cli/settings.rb +0 -30
- data/lib/kward/cli/slash_commands.rb +38 -11
- data/lib/kward/cli.rb +14 -0
- data/lib/kward/compactor.rb +4 -1
- data/lib/kward/config_files.rb +4 -6
- data/lib/kward/conversation.rb +49 -5
- data/lib/kward/model/client.rb +37 -50
- data/lib/kward/model/context_usage.rb +13 -6
- data/lib/kward/model/model_info.rb +92 -9
- data/lib/kward/model/payloads.rb +2 -0
- data/lib/kward/openrouter_model_cache.rb +120 -0
- data/lib/kward/plugin_registry.rb +47 -1
- data/lib/kward/prompt_interface/banner.rb +16 -51
- data/lib/kward/prompt_interface/composer_controller.rb +60 -87
- data/lib/kward/prompt_interface/composer_renderer.rb +7 -1
- data/lib/kward/prompt_interface/key_handler.rb +31 -10
- data/lib/kward/prompt_interface/layout.rb +2 -2
- data/lib/kward/prompt_interface/overlay_renderer.rb +24 -0
- data/lib/kward/prompt_interface/prompt_renderer.rb +23 -2
- data/lib/kward/prompt_interface/question_prompt.rb +34 -42
- data/lib/kward/prompt_interface/runtime_state.rb +6 -1
- data/lib/kward/prompt_interface/screen.rb +10 -4
- data/lib/kward/prompt_interface/selection_prompt.rb +518 -61
- data/lib/kward/prompt_interface/slash_overlay.rb +4 -4
- data/lib/kward/prompt_interface/transcript_buffer.rb +7 -16
- data/lib/kward/prompt_interface/transcript_renderer.rb +3 -3
- data/lib/kward/prompt_interface.rb +31 -32
- data/lib/kward/prompts/commands.rb +6 -3
- data/lib/kward/prompts.rb +2 -2
- data/lib/kward/rpc/server.rb +3 -8
- data/lib/kward/rpc/session_manager.rb +19 -8
- data/lib/kward/session_diff.rb +106 -9
- data/lib/kward/session_store.rb +23 -4
- data/lib/kward/session_tree_renderer.rb +2 -1
- data/lib/kward/telemetry/logger.rb +5 -3
- data/lib/kward/tool_output_compactor.rb +127 -0
- data/lib/kward/tools/base.rb +8 -2
- data/lib/kward/tools/registry.rb +37 -6
- data/lib/kward/tools/retrieve_tool_output.rb +71 -0
- data/lib/kward/tools/search/web.rb +2 -2
- data/lib/kward/tools/summarize_file_structure.rb +29 -0
- data/lib/kward/tools/tool_call.rb +2 -0
- data/lib/kward/version.rb +1 -1
- data/lib/kward/workspace.rb +58 -2
- data/templates/default/fulldoc/html/css/kward.css +570 -78
- data/templates/default/fulldoc/html/full_list.erb +107 -0
- data/templates/default/fulldoc/html/js/kward.js +259 -97
- data/templates/default/fulldoc/html/setup.rb +8 -0
- data/templates/default/kward_navigation.rb +91 -0
- data/templates/default/layout/html/layout.erb +59 -13
- data/templates/default/layout/html/setup.rb +34 -39
- metadata +13 -3
- data/lib/kward/resources/avatar_kward_logo.rb +0 -50
- data/lib/kward/resources/pixel_logo.rb +0 -232
|
@@ -10,9 +10,6 @@ module Kward
|
|
|
10
10
|
return select_current_choice if key.nil?
|
|
11
11
|
return if handle_select_bracketed_paste_key(key)
|
|
12
12
|
|
|
13
|
-
csi_result = handle_select_csi_u_key(key)
|
|
14
|
-
return csi_result unless csi_result == false
|
|
15
|
-
|
|
16
13
|
if key.is_a?(String) && key.length > 1
|
|
17
14
|
token = next_key_token(key)
|
|
18
15
|
if token.length < key.length
|
|
@@ -21,22 +18,32 @@ module Kward
|
|
|
21
18
|
end
|
|
22
19
|
end
|
|
23
20
|
|
|
21
|
+
return handle_select_confirmation_key(key) if select_confirmation_active?
|
|
22
|
+
|
|
23
|
+
csi_result = handle_select_csi_u_key(key)
|
|
24
|
+
return csi_result unless csi_result == false
|
|
25
|
+
|
|
26
|
+
return handle_select_input_key(key) if select_input_active?
|
|
27
|
+
|
|
28
|
+
binding_result = handle_select_search_key_binding(key)
|
|
29
|
+
return binding_result unless binding_result == false
|
|
30
|
+
|
|
24
31
|
key_name = @reader.console.keys[key]
|
|
25
32
|
case key_name
|
|
26
33
|
when :return, :enter
|
|
27
34
|
select_current_choice
|
|
28
35
|
when :backspace
|
|
29
|
-
select_delete_before_cursor
|
|
36
|
+
select_delete_before_cursor if select_search_active?
|
|
30
37
|
when :delete
|
|
31
|
-
select_delete_at_cursor
|
|
38
|
+
select_delete_at_cursor if select_search_active?
|
|
32
39
|
when :left
|
|
33
|
-
|
|
40
|
+
select_move_cursor_left if select_search_active?
|
|
34
41
|
when :right
|
|
35
|
-
|
|
42
|
+
select_move_cursor_right if select_search_active?
|
|
36
43
|
when :home
|
|
37
|
-
self.composer_cursor = 0
|
|
44
|
+
self.composer_cursor = 0 if select_search_active?
|
|
38
45
|
when :end
|
|
39
|
-
self.composer_cursor = composer_input.length
|
|
46
|
+
self.composer_cursor = composer_input.length if select_search_active?
|
|
40
47
|
when :up
|
|
41
48
|
select_previous_choice
|
|
42
49
|
when :down
|
|
@@ -46,69 +53,111 @@ module Kward
|
|
|
46
53
|
when "\n", "\r"
|
|
47
54
|
select_current_choice
|
|
48
55
|
when "\b", "\x7F"
|
|
49
|
-
select_delete_before_cursor
|
|
56
|
+
select_delete_before_cursor if select_search_active?
|
|
50
57
|
when "\e"
|
|
51
58
|
handle_select_escape_sequence
|
|
52
59
|
else
|
|
53
|
-
|
|
60
|
+
select_typed_key(key)
|
|
54
61
|
end
|
|
55
62
|
end
|
|
56
63
|
end
|
|
57
64
|
|
|
65
|
+
def drain_pending_select_keys_locked(result)
|
|
66
|
+
until result.is_a?(String) || select_action_result?(result) || result == SELECT_CANCEL || @pending_keys.empty?
|
|
67
|
+
result = handle_select_key(@pending_keys.shift)
|
|
68
|
+
end
|
|
69
|
+
result
|
|
70
|
+
end
|
|
71
|
+
|
|
58
72
|
def handle_select_csi_u_key(key)
|
|
59
|
-
|
|
60
|
-
return false unless
|
|
73
|
+
sequence = parse_csi_u_key(key)
|
|
74
|
+
return false unless sequence
|
|
61
75
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
76
|
+
code = sequence[:code]
|
|
77
|
+
modifiers = sequence[:modifiers]
|
|
78
|
+
modifier = sequence[:modifier]
|
|
79
|
+
queue_pending_keys(sequence[:remaining]) if sequence[:remaining] && !sequence[:remaining].empty?
|
|
65
80
|
|
|
66
81
|
case code
|
|
67
82
|
when 13
|
|
68
|
-
select_current_choice
|
|
83
|
+
select_input_active? ? select_input_action_result : select_current_choice
|
|
69
84
|
when 27
|
|
70
|
-
|
|
85
|
+
if select_input_active?
|
|
86
|
+
clear_select_input
|
|
87
|
+
elsif select_search_active?
|
|
88
|
+
select_cancel_search
|
|
89
|
+
else
|
|
90
|
+
SELECT_CANCEL
|
|
91
|
+
end
|
|
71
92
|
when 8, 127
|
|
72
|
-
|
|
93
|
+
if select_editing_active?
|
|
94
|
+
alt_modifier?(modifier) ? select_delete_word_before_cursor : select_delete_before_cursor
|
|
95
|
+
end
|
|
96
|
+
nil
|
|
97
|
+
when 4
|
|
98
|
+
select_delete_at_cursor if select_editing_active?
|
|
73
99
|
nil
|
|
100
|
+
else
|
|
101
|
+
modified_result = handle_select_modified_csi_u_key(code, modifier)
|
|
102
|
+
return modified_result unless modified_result == false
|
|
103
|
+
|
|
104
|
+
handle_select_printable_csi_u_key(code, modifiers)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def handle_select_modified_csi_u_key(code, modifier)
|
|
109
|
+
return false unless select_editing_active?
|
|
110
|
+
return false unless ctrl_modifier?(modifier) || alt_modifier?(modifier)
|
|
111
|
+
|
|
112
|
+
normalized_code = code.to_i.chr.downcase.ord rescue code
|
|
113
|
+
if ctrl_modifier?(modifier)
|
|
114
|
+
handle_select_ctrl_key(normalized_code)
|
|
115
|
+
elsif alt_modifier?(modifier)
|
|
116
|
+
handle_select_alt_key(normalized_code)
|
|
74
117
|
else
|
|
75
118
|
false
|
|
76
119
|
end
|
|
77
120
|
end
|
|
78
121
|
|
|
122
|
+
def handle_select_printable_csi_u_key(code, modifiers)
|
|
123
|
+
return false unless modifiers.empty? || modifiers == "1"
|
|
124
|
+
return false unless code.between?(32, 126)
|
|
125
|
+
|
|
126
|
+
key = code.chr(Encoding::UTF_8)
|
|
127
|
+
select_typed_key(key)
|
|
128
|
+
end
|
|
129
|
+
|
|
79
130
|
def handle_select_escape_sequence
|
|
80
|
-
|
|
81
|
-
return
|
|
131
|
+
pending_sequence = read_pending_escape_sequence
|
|
132
|
+
return clear_select_input if pending_sequence.empty? && select_input_active?
|
|
133
|
+
return select_cancel_search if pending_sequence.empty? && select_search_active?
|
|
134
|
+
return SELECT_CANCEL if pending_sequence.empty?
|
|
135
|
+
|
|
136
|
+
full_sequence = "\e#{pending_sequence}"
|
|
137
|
+
sequence = next_key_token(full_sequence)
|
|
138
|
+
queue_pending_keys(full_sequence[sequence.length..]) if full_sequence.length > sequence.length
|
|
139
|
+
return SELECT_CANCEL if sequence == "\e"
|
|
82
140
|
|
|
83
|
-
key_name = @reader.console.keys[
|
|
141
|
+
key_name = @reader.console.keys[sequence]
|
|
84
142
|
case key_name
|
|
85
143
|
when :up
|
|
86
144
|
select_previous_choice
|
|
87
145
|
when :down
|
|
88
146
|
select_next_choice
|
|
89
147
|
when :left
|
|
90
|
-
|
|
148
|
+
select_move_cursor_left if select_editing_active?
|
|
91
149
|
when :right
|
|
92
|
-
|
|
150
|
+
select_move_cursor_right if select_editing_active?
|
|
93
151
|
end
|
|
94
152
|
true
|
|
95
153
|
end
|
|
96
154
|
|
|
97
155
|
def handle_select_bracketed_paste_key(key)
|
|
98
|
-
|
|
99
|
-
return false unless
|
|
156
|
+
paste = read_bracketed_paste(key)
|
|
157
|
+
return false unless paste
|
|
100
158
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
chunk = @reader.read_keypress(echo: false, raw: true)
|
|
104
|
-
break if chunk.nil?
|
|
105
|
-
|
|
106
|
-
pasted << chunk.to_s
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
content, remaining = pasted.split(BRACKETED_PASTE_END, 2)
|
|
110
|
-
select_insert_string(normalize_paste(content || ""))
|
|
111
|
-
queue_pending_keys(remaining) if remaining && !remaining.empty?
|
|
159
|
+
select_insert_string(normalize_paste(paste[:content])) if select_editing_active?
|
|
160
|
+
queue_pending_keys(paste[:remaining]) if paste[:remaining] && !paste[:remaining].empty?
|
|
112
161
|
true
|
|
113
162
|
end
|
|
114
163
|
|
|
@@ -116,8 +165,233 @@ module Kward
|
|
|
116
165
|
selected_selection_choice || custom_selection_choice || SELECT_CANCEL
|
|
117
166
|
end
|
|
118
167
|
|
|
168
|
+
def handle_select_confirmation_key(key)
|
|
169
|
+
if key.to_s.start_with?("\e")
|
|
170
|
+
clear_select_confirmation
|
|
171
|
+
return true
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
key == @select_state[:confirm_key] ? select_action_key(key) : true
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def handle_select_input_key(key)
|
|
178
|
+
key_name = @reader.console.keys[key]
|
|
179
|
+
case key_name
|
|
180
|
+
when :return, :enter
|
|
181
|
+
select_input_action_result
|
|
182
|
+
when :backspace
|
|
183
|
+
select_delete_before_cursor
|
|
184
|
+
when :delete
|
|
185
|
+
select_delete_at_cursor
|
|
186
|
+
when :left
|
|
187
|
+
select_move_cursor_left
|
|
188
|
+
when :right
|
|
189
|
+
select_move_cursor_right
|
|
190
|
+
when :home
|
|
191
|
+
self.composer_cursor = 0
|
|
192
|
+
when :end
|
|
193
|
+
self.composer_cursor = composer_input.length
|
|
194
|
+
else
|
|
195
|
+
case key
|
|
196
|
+
when "\n", "\r"
|
|
197
|
+
select_input_action_result
|
|
198
|
+
when "\b", "\x7F"
|
|
199
|
+
select_delete_before_cursor
|
|
200
|
+
when "\e"
|
|
201
|
+
clear_select_input
|
|
202
|
+
true
|
|
203
|
+
else
|
|
204
|
+
handle_select_search_key_binding(key) || select_insert_key(key)
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def select_action_key(key)
|
|
210
|
+
return nil unless key.is_a?(String) && key.length == 1
|
|
211
|
+
|
|
212
|
+
action_keys = @select_state ? @select_state[:action_keys].to_h : {}
|
|
213
|
+
action = action_keys[key]
|
|
214
|
+
choice = selected_selection_choice
|
|
215
|
+
return nil unless action && choice
|
|
216
|
+
|
|
217
|
+
if select_confirmation_active?
|
|
218
|
+
return nil unless key == @select_state[:confirm_key]
|
|
219
|
+
|
|
220
|
+
clear_select_confirmation
|
|
221
|
+
return action.merge(choice: choice).reject { |name, _value| name == :confirm || name == :confirm_title }
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
if action[:confirm]
|
|
225
|
+
@select_state[:confirm_key] = key
|
|
226
|
+
@select_state[:confirm_text] = action[:confirm].to_s
|
|
227
|
+
@select_state[:confirm_title] = action[:confirm_title].to_s
|
|
228
|
+
return true
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
if action[:input_prompt]
|
|
232
|
+
@select_state[:input_action] = action
|
|
233
|
+
@select_state[:input_choice] = choice
|
|
234
|
+
@select_state[:input_prompt_label] = @prompt_label
|
|
235
|
+
@prompt_label = action[:input_prompt].to_s
|
|
236
|
+
self.composer_input = ""
|
|
237
|
+
self.composer_cursor = 0
|
|
238
|
+
return true
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
action.merge(choice: choice)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def select_confirmation_active?
|
|
245
|
+
@select_state && !@select_state[:confirm_key].to_s.empty?
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def select_input_active?
|
|
249
|
+
@select_state && @select_state[:input_action]
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def select_input_action_result
|
|
253
|
+
return unless @select_state
|
|
254
|
+
|
|
255
|
+
action = @select_state[:input_action].dup
|
|
256
|
+
choice = @select_state[:input_choice]
|
|
257
|
+
input = composer_input.strip
|
|
258
|
+
clear_select_input
|
|
259
|
+
action.merge(choice: choice, input: input).reject { |name, _value| name == :input_prompt }
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def clear_select_input
|
|
263
|
+
return unless @select_state
|
|
264
|
+
|
|
265
|
+
@prompt_label = @select_state[:input_prompt_label].to_s unless @select_state[:input_prompt_label].to_s.empty?
|
|
266
|
+
@select_state.delete(:input_action)
|
|
267
|
+
@select_state.delete(:input_choice)
|
|
268
|
+
@select_state.delete(:input_prompt_label)
|
|
269
|
+
self.composer_input = ""
|
|
270
|
+
self.composer_cursor = 0
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def clear_select_confirmation
|
|
274
|
+
return unless @select_state
|
|
275
|
+
|
|
276
|
+
@select_state.delete(:confirm_key)
|
|
277
|
+
@select_state.delete(:confirm_text)
|
|
278
|
+
@select_state.delete(:confirm_title)
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def select_action_result?(result)
|
|
282
|
+
result.is_a?(Hash) && result.key?(:action) && result.key?(:choice)
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def select_action_handler(result, action_handlers)
|
|
286
|
+
action_handlers.to_h[result[:action]] || action_handlers.to_h[result[:action].to_s]
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def run_select_action(result, handler)
|
|
290
|
+
begin_select_action(result[:activity])
|
|
291
|
+
minimum_busy_until = result[:activity] ? monotonic_now + SELECT_ACTION_MINIMUM_BUSY_SECONDS : nil
|
|
292
|
+
action_result = nil
|
|
293
|
+
action_error = nil
|
|
294
|
+
worker = Thread.new do
|
|
295
|
+
action_result = if result.key?(:input) && handler.arity != 1
|
|
296
|
+
handler.call(result[:choice], result[:input])
|
|
297
|
+
else
|
|
298
|
+
handler.call(result[:choice])
|
|
299
|
+
end
|
|
300
|
+
rescue StandardError => e
|
|
301
|
+
action_error = e
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
while worker.alive? || (minimum_busy_until && monotonic_now < minimum_busy_until)
|
|
305
|
+
tick_select_action_locked
|
|
306
|
+
sleep 0.02
|
|
307
|
+
end
|
|
308
|
+
worker.join
|
|
309
|
+
raise action_error if action_error
|
|
310
|
+
|
|
311
|
+
apply_select_action_result(action_result)
|
|
312
|
+
ensure
|
|
313
|
+
finish_select_action
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def apply_select_action_result(result)
|
|
317
|
+
return SELECT_CONTINUE if result == SELECT_CONTINUE
|
|
318
|
+
return result unless select_continue_result?(result)
|
|
319
|
+
|
|
320
|
+
@mutex.synchronize do
|
|
321
|
+
if @select_state
|
|
322
|
+
@select_state[:choices] = Array(result[:choices]).map(&:to_s) if result.key?(:choices)
|
|
323
|
+
@select_state[:selection_index] = result[:selection_index].to_i if result.key?(:selection_index)
|
|
324
|
+
end
|
|
325
|
+
render_prompt_locked
|
|
326
|
+
@output_io.flush
|
|
327
|
+
end
|
|
328
|
+
SELECT_CONTINUE
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def select_continue_result?(result)
|
|
332
|
+
result.is_a?(Hash) && result[:select_continue]
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def begin_select_action(activity)
|
|
336
|
+
return if activity.to_s.empty?
|
|
337
|
+
|
|
338
|
+
@mutex.synchronize do
|
|
339
|
+
@busy = true
|
|
340
|
+
@busy_activity = normalize_busy_activity(activity)
|
|
341
|
+
@asking = true
|
|
342
|
+
reset_spinner_locked
|
|
343
|
+
render_prompt_locked
|
|
344
|
+
@output_io.flush
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def finish_select_action
|
|
349
|
+
@mutex.synchronize do
|
|
350
|
+
@busy = false
|
|
351
|
+
@busy_activity = "streaming"
|
|
352
|
+
@select_state&.delete(:busy_activity)
|
|
353
|
+
render_prompt_locked if @asking
|
|
354
|
+
@output_io.flush
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def tick_select_action_locked
|
|
359
|
+
@mutex.synchronize do
|
|
360
|
+
resized = handle_resize_locked
|
|
361
|
+
spun = tick_spinner_locked
|
|
362
|
+
footer_refreshed = tick_footer_locked
|
|
363
|
+
render_prompt_locked if resized || spun || footer_refreshed
|
|
364
|
+
@output_io.flush if resized || spun || footer_refreshed
|
|
365
|
+
end
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
def normalized_select_action_keys(action_keys)
|
|
369
|
+
action_keys.to_h.each_with_object({}) do |(key, action), normalized|
|
|
370
|
+
next unless key.to_s.length == 1
|
|
371
|
+
|
|
372
|
+
normalized_action = normalized_select_action(action)
|
|
373
|
+
normalized[key.to_s] = normalized_action if normalized_action
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
def normalized_select_action(action)
|
|
378
|
+
if action.is_a?(Hash)
|
|
379
|
+
name = action[:action] || action["action"]
|
|
380
|
+
activity = action[:activity] || action["activity"]
|
|
381
|
+
confirm = action[:confirm] || action["confirm"]
|
|
382
|
+
confirm_title = action[:confirm_title] || action["confirm_title"]
|
|
383
|
+
input_prompt = action[:input_prompt] || action["input_prompt"]
|
|
384
|
+
defer_finish_render = action[:defer_finish_render] || action["defer_finish_render"]
|
|
385
|
+
else
|
|
386
|
+
name = action
|
|
387
|
+
end
|
|
388
|
+
return nil if name.to_s.empty?
|
|
389
|
+
|
|
390
|
+
{ action: name.to_sym, activity: activity.to_s, confirm: confirm.to_s, confirm_title: confirm_title.to_s, input_prompt: input_prompt.to_s, defer_finish_render: defer_finish_render }.delete_if { |_key, value| value.to_s.empty? }
|
|
391
|
+
end
|
|
392
|
+
|
|
119
393
|
def custom_selection_choice
|
|
120
|
-
return nil unless @select_state && @select_state[:custom]
|
|
394
|
+
return nil unless @select_state && @select_state[:custom] && select_search_active?
|
|
121
395
|
|
|
122
396
|
value = composer_input.strip
|
|
123
397
|
value.empty? ? nil : value
|
|
@@ -134,14 +408,176 @@ module Kward
|
|
|
134
408
|
matches = selection_matches
|
|
135
409
|
return if matches.empty?
|
|
136
410
|
|
|
137
|
-
@select_state[:selection_index] = (selection_index
|
|
411
|
+
@select_state[:selection_index] = previous_list_selection_index(selection_index, matches.length)
|
|
138
412
|
end
|
|
139
413
|
|
|
140
414
|
def select_next_choice
|
|
141
415
|
matches = selection_matches
|
|
142
416
|
return if matches.empty?
|
|
143
417
|
|
|
144
|
-
@select_state[:selection_index] = (selection_index
|
|
418
|
+
@select_state[:selection_index] = next_list_selection_index(selection_index, matches.length)
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
def handle_select_search_key_binding(key)
|
|
422
|
+
return false unless select_editing_active?
|
|
423
|
+
|
|
424
|
+
case key
|
|
425
|
+
when "\x01"
|
|
426
|
+
select_move_to_start_of_line
|
|
427
|
+
when "\x02"
|
|
428
|
+
select_move_cursor_left
|
|
429
|
+
when "\x04"
|
|
430
|
+
select_delete_at_cursor
|
|
431
|
+
when "\x05"
|
|
432
|
+
select_move_to_end_of_line
|
|
433
|
+
when "\x06"
|
|
434
|
+
select_move_cursor_right
|
|
435
|
+
when "\x08"
|
|
436
|
+
select_delete_before_cursor
|
|
437
|
+
when "\x0B"
|
|
438
|
+
select_kill_line_after_cursor
|
|
439
|
+
when "\x0C"
|
|
440
|
+
redraw_screen_locked
|
|
441
|
+
when "\x15"
|
|
442
|
+
select_kill_line_before_cursor
|
|
443
|
+
when "\x17"
|
|
444
|
+
select_delete_word_before_cursor
|
|
445
|
+
when "\x19"
|
|
446
|
+
select_yank_kill_buffer
|
|
447
|
+
when "\eb"
|
|
448
|
+
select_move_to_previous_word
|
|
449
|
+
when "\ed"
|
|
450
|
+
select_delete_word_after_cursor
|
|
451
|
+
when "\ef"
|
|
452
|
+
select_move_to_next_word
|
|
453
|
+
when "\e\b", "\e\x7F"
|
|
454
|
+
select_delete_word_before_cursor
|
|
455
|
+
else
|
|
456
|
+
false
|
|
457
|
+
end
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
def handle_select_ctrl_key(code)
|
|
461
|
+
case code
|
|
462
|
+
when 97
|
|
463
|
+
select_move_to_start_of_line
|
|
464
|
+
when 98
|
|
465
|
+
select_move_cursor_left
|
|
466
|
+
when 100
|
|
467
|
+
select_delete_at_cursor
|
|
468
|
+
when 101
|
|
469
|
+
select_move_to_end_of_line
|
|
470
|
+
when 102
|
|
471
|
+
select_move_cursor_right
|
|
472
|
+
when 104
|
|
473
|
+
select_delete_before_cursor
|
|
474
|
+
when 107
|
|
475
|
+
select_kill_line_after_cursor
|
|
476
|
+
when 108
|
|
477
|
+
redraw_screen_locked
|
|
478
|
+
when 117
|
|
479
|
+
select_kill_line_before_cursor
|
|
480
|
+
when 119
|
|
481
|
+
select_delete_word_before_cursor
|
|
482
|
+
when 121
|
|
483
|
+
select_yank_kill_buffer
|
|
484
|
+
else
|
|
485
|
+
false
|
|
486
|
+
end
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
def handle_select_alt_key(code)
|
|
490
|
+
case code
|
|
491
|
+
when 98
|
|
492
|
+
select_move_to_previous_word
|
|
493
|
+
when 100
|
|
494
|
+
select_delete_word_after_cursor
|
|
495
|
+
when 102
|
|
496
|
+
select_move_to_next_word
|
|
497
|
+
else
|
|
498
|
+
false
|
|
499
|
+
end
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
def select_typed_key(key)
|
|
503
|
+
return select_insert_key(key) if select_input_active?
|
|
504
|
+
return select_begin_search if key == "/" && !select_search_active?
|
|
505
|
+
return select_action_key(key) unless select_search_active?
|
|
506
|
+
|
|
507
|
+
select_insert_key(key)
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
def select_begin_search
|
|
511
|
+
return unless @select_state
|
|
512
|
+
|
|
513
|
+
@select_state[:search_active] = true
|
|
514
|
+
self.composer_input = ""
|
|
515
|
+
self.composer_cursor = 0
|
|
516
|
+
true
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
def select_cancel_search
|
|
520
|
+
return unless @select_state
|
|
521
|
+
|
|
522
|
+
@select_state[:search_active] = false
|
|
523
|
+
self.composer_input = ""
|
|
524
|
+
self.composer_cursor = 0
|
|
525
|
+
@select_state[:selection_index] = 0
|
|
526
|
+
true
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
def select_search_active?
|
|
530
|
+
@select_state && @select_state[:search_active]
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
def select_editing_active?
|
|
534
|
+
select_search_active? || select_input_active?
|
|
535
|
+
end
|
|
536
|
+
|
|
537
|
+
def select_move_cursor_left
|
|
538
|
+
@composer.move_cursor_left
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
def select_move_cursor_right
|
|
542
|
+
@composer.move_cursor_right
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
def select_move_to_start_of_line
|
|
546
|
+
@composer.move_to_start_of_line
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
def select_move_to_end_of_line
|
|
550
|
+
@composer.move_to_end_of_line
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
def select_move_to_previous_word
|
|
554
|
+
@composer.move_to_previous_word
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
def select_move_to_next_word
|
|
558
|
+
@composer.move_to_next_word
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
def select_delete_word_before_cursor
|
|
562
|
+
reset_select_filter if @composer.delete_word_before_cursor
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
def select_delete_word_after_cursor
|
|
566
|
+
reset_select_filter if @composer.delete_word_after_cursor
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
def select_kill_line_before_cursor
|
|
570
|
+
reset_select_filter if @composer.kill_line_before_cursor
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
def select_kill_line_after_cursor
|
|
574
|
+
reset_select_filter if @composer.kill_line_after_cursor
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
def select_yank_kill_buffer
|
|
578
|
+
before = composer_input
|
|
579
|
+
@composer.yank_kill_buffer
|
|
580
|
+
reset_select_filter unless composer_input == before
|
|
145
581
|
end
|
|
146
582
|
|
|
147
583
|
def select_insert_key(key)
|
|
@@ -153,29 +589,25 @@ module Kward
|
|
|
153
589
|
def select_insert_string(string)
|
|
154
590
|
return if string.empty?
|
|
155
591
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
@select_state[:selection_index] = 0 if @select_state
|
|
592
|
+
@composer.insert_string(string)
|
|
593
|
+
reset_select_filter
|
|
159
594
|
end
|
|
160
595
|
|
|
161
596
|
def select_delete_before_cursor
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
self.composer_input = composer_input[0...(composer_cursor - 1)] + composer_input[composer_cursor..]
|
|
165
|
-
self.composer_cursor -= 1
|
|
166
|
-
@select_state[:selection_index] = 0 if @select_state
|
|
597
|
+
reset_select_filter if @composer.delete_before_cursor
|
|
167
598
|
end
|
|
168
599
|
|
|
169
600
|
def select_delete_at_cursor
|
|
170
|
-
|
|
601
|
+
reset_select_filter if @composer.delete_at_cursor
|
|
602
|
+
end
|
|
171
603
|
|
|
172
|
-
|
|
173
|
-
@select_state[:selection_index] = 0 if @select_state
|
|
604
|
+
def reset_select_filter
|
|
605
|
+
@select_state[:selection_index] = 0 if @select_state && !select_input_active?
|
|
174
606
|
end
|
|
175
607
|
|
|
176
608
|
def selection_matches
|
|
177
609
|
choices = @select_state ? @select_state[:choices] : []
|
|
178
|
-
filter = composer_input.downcase.strip
|
|
610
|
+
filter = select_search_active? ? composer_input.downcase.strip : ""
|
|
179
611
|
matches = filter.empty? ? choices : choices.select { |choice| choice.downcase.include?(filter) }
|
|
180
612
|
clamp_selection_index(matches.length)
|
|
181
613
|
matches
|
|
@@ -192,24 +624,28 @@ module Kward
|
|
|
192
624
|
@select_state[:selection_index] = count - 1 if count.positive? && selection_index >= count
|
|
193
625
|
end
|
|
194
626
|
|
|
195
|
-
def finish_select_prompt
|
|
627
|
+
def finish_select_prompt(render: true)
|
|
196
628
|
@mutex.synchronize do
|
|
197
629
|
@select_state = nil
|
|
198
|
-
clear_prompt_locked
|
|
199
630
|
self.composer_input = ""
|
|
200
631
|
self.composer_cursor = 0
|
|
201
|
-
@asking =
|
|
202
|
-
|
|
203
|
-
@cursor_rendered_row = 0
|
|
632
|
+
@asking = true
|
|
633
|
+
render_prompt_locked if render
|
|
204
634
|
@output_io.flush
|
|
205
635
|
end
|
|
206
636
|
end
|
|
207
637
|
|
|
638
|
+
def select_deferred_finish_render?(result)
|
|
639
|
+
result.is_a?(Hash) && result[:defer_finish_render]
|
|
640
|
+
end
|
|
641
|
+
|
|
208
642
|
def selection_overlay_rows(width, height: screen_height)
|
|
643
|
+
return selection_confirmation_rows(width) if select_confirmation_active?
|
|
644
|
+
|
|
209
645
|
matches = selection_matches
|
|
210
|
-
lines = [overlay_text_line(
|
|
646
|
+
lines = [overlay_text_line(selection_overlay_help_text, :muted), overlay_blank_line]
|
|
211
647
|
if matches.empty?
|
|
212
|
-
if @select_state && @select_state[:custom] && !composer_input.strip.empty?
|
|
648
|
+
if @select_state && @select_state[:custom] && select_search_active? && !composer_input.strip.empty?
|
|
213
649
|
lines << overlay_choice_line("Use custom: #{composer_input.strip}", selected: true)
|
|
214
650
|
else
|
|
215
651
|
lines << overlay_text_line("No matches", :muted)
|
|
@@ -226,14 +662,35 @@ module Kward
|
|
|
226
662
|
overlay_card_rows(selection_overlay_title, lines, width)
|
|
227
663
|
end
|
|
228
664
|
|
|
665
|
+
def selection_overlay_help_text
|
|
666
|
+
return "Renaming · Enter save · Esc cancel" if select_input_active?
|
|
667
|
+
|
|
668
|
+
text = "↑/↓ select · Enter open"
|
|
669
|
+
text = "#{text} · / search" unless select_search_active?
|
|
670
|
+
action_keys = @select_state ? @select_state[:action_keys].to_h : {}
|
|
671
|
+
action_keys.each do |key, action|
|
|
672
|
+
text = "#{text} · #{key} #{action[:action]}"
|
|
673
|
+
end
|
|
674
|
+
"#{text} · Esc cancel"
|
|
675
|
+
end
|
|
676
|
+
|
|
229
677
|
def selection_overlay_title
|
|
230
678
|
title = @select_state && @select_state[:title].to_s
|
|
231
679
|
title && !title.empty? ? title : "Sessions"
|
|
232
680
|
end
|
|
233
681
|
|
|
682
|
+
def selection_confirmation_rows(width)
|
|
683
|
+
title = @select_state[:confirm_title].to_s
|
|
684
|
+
title = "Confirm" if title.empty?
|
|
685
|
+
text = @select_state[:confirm_text].to_s
|
|
686
|
+
text = "Press #{@select_state[:confirm_key]} again to confirm, Esc to cancel." if text.empty?
|
|
687
|
+
lines = [overlay_text_line(text, :muted)]
|
|
688
|
+
overlay_card_rows(title, lines, width)
|
|
689
|
+
end
|
|
690
|
+
|
|
234
691
|
def visible_selection_matches(matches, height: screen_height)
|
|
235
|
-
max_rows =
|
|
236
|
-
start =
|
|
692
|
+
max_rows = max_overlay_list_rows(height)
|
|
693
|
+
start = centered_list_window_start(selection_index, matches.length, max_rows)
|
|
237
694
|
{ start: start, choices: matches[start, max_rows] || [] }
|
|
238
695
|
end
|
|
239
696
|
|