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