kward 0.71.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/CHANGELOG.md +41 -1
- data/Gemfile.lock +2 -2
- data/README.md +4 -0
- data/doc/agent-tools.md +15 -6
- data/doc/authentication.md +22 -1
- data/doc/code-search.md +42 -2
- data/doc/configuration.md +106 -3
- data/doc/context-budgeting.md +136 -0
- data/doc/context-tools.md +16 -3
- 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 +72 -1
- data/doc/releasing.md +37 -9
- data/doc/rpc.md +74 -4
- data/doc/session-management.md +35 -1
- data/doc/shell.md +286 -0
- data/doc/tabs.md +122 -0
- data/doc/troubleshooting.md +77 -1
- data/doc/usage.md +53 -7
- data/doc/web-search.md +12 -4
- data/doc/workspace-tools.md +51 -12
- data/examples/plugins/space_invaders.rb +377 -0
- data/lib/kward/agent.rb +1 -1
- data/lib/kward/cli/commands.rb +33 -2
- data/lib/kward/cli/git.rb +150 -0
- data/lib/kward/cli/interactive_turn.rb +73 -9
- data/lib/kward/cli/plugins.rb +54 -4
- data/lib/kward/cli/prompt_interface.rb +32 -1
- data/lib/kward/cli/runtime_helpers.rb +133 -3
- data/lib/kward/cli/sessions.rb +2 -2
- data/lib/kward/cli/settings.rb +218 -9
- data/lib/kward/cli/slash_commands.rb +415 -2
- data/lib/kward/cli/tabs.rb +695 -0
- data/lib/kward/cli.rb +158 -26
- data/lib/kward/config_files.rb +123 -1
- data/lib/kward/context_budget_meter.rb +44 -0
- data/lib/kward/conversation.rb +12 -4
- data/lib/kward/editor_mode.rb +25 -0
- data/lib/kward/ekwsh.rb +362 -0
- data/lib/kward/plugin_registry.rb +61 -0
- data/lib/kward/project_files.rb +52 -0
- data/lib/kward/prompt_history.rb +82 -0
- data/lib/kward/prompt_interface/composer_controller.rb +69 -1
- data/lib/kward/prompt_interface/composer_renderer.rb +109 -13
- 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 +387 -35
- 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/question_prompt.rb +98 -50
- data/lib/kward/prompt_interface/runtime_state.rb +43 -0
- data/lib/kward/prompt_interface/screen.rb +16 -0
- data/lib/kward/prompt_interface/selection_prompt.rb +7 -13
- data/lib/kward/prompt_interface/stream_state.rb +7 -0
- data/lib/kward/prompt_interface/transcript_buffer.rb +6 -0
- data/lib/kward/prompt_interface.rb +286 -8
- data/lib/kward/prompts/commands.rb +5 -0
- data/lib/kward/prompts.rb +2 -0
- data/lib/kward/rpc/server.rb +42 -3
- data/lib/kward/rpc/session_manager.rb +35 -47
- 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 +44 -0
- 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/text_boundary.rb +25 -0
- 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 +62 -16
- data/lib/kward/tools/tool_call.rb +10 -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 +110 -24
- data/templates/default/fulldoc/html/css/kward.css +107 -36
- data/templates/default/kward_navigation.rb +12 -1
- data/templates/default/layout/html/layout.erb +4 -2
- data/templates/default/layout/html/setup.rb +6 -0
- metadata +53 -1
|
@@ -10,6 +10,8 @@ module Kward
|
|
|
10
10
|
pending = @pending_keys.shift unless @pending_keys.empty?
|
|
11
11
|
return pending if pending
|
|
12
12
|
|
|
13
|
+
return nil if nonblock && @input_io.respond_to?(:wait_readable) && !@input_io.wait_readable(0)
|
|
14
|
+
|
|
13
15
|
@reader.read_keypress(echo: false, raw: true, nonblock: nonblock)
|
|
14
16
|
rescue TTY::Reader::InputInterrupt
|
|
15
17
|
"\x03"
|
|
@@ -17,28 +19,43 @@ module Kward
|
|
|
17
19
|
nil
|
|
18
20
|
end
|
|
19
21
|
|
|
22
|
+
def handle_editor_input_key(key)
|
|
23
|
+
tab_result = handle_tab_key_binding(key)
|
|
24
|
+
return tab_result unless tab_result == false
|
|
25
|
+
|
|
26
|
+
result = handle_editor_key(key)
|
|
27
|
+
result.is_a?(String) ? true : result
|
|
28
|
+
end
|
|
29
|
+
|
|
20
30
|
def handle_key(key)
|
|
21
31
|
return submit_input if key.nil?
|
|
32
|
+
return handle_interactive_key(key) if interactive_active_locked?
|
|
33
|
+
return handle_editor_input_key(key) if editor_active?
|
|
34
|
+
return handle_project_browser_key(key) if project_browser_visible?
|
|
35
|
+
return handle_history_search_key(key) if history_search_active?
|
|
36
|
+
return true if handle_mouse_reporting_key(key)
|
|
22
37
|
return if handle_bracketed_paste_key(key)
|
|
23
38
|
|
|
24
39
|
csi_result = handle_csi_u_key(key)
|
|
25
40
|
return csi_result unless csi_result == false
|
|
26
41
|
return if handle_shift_enter_key(key)
|
|
27
|
-
if key
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
42
|
+
return true if handle_bundled_key(key) { |token| handle_key(token) }
|
|
43
|
+
|
|
44
|
+
completion_result = handle_completion_provider_key(key)
|
|
45
|
+
return completion_result unless completion_result == false
|
|
46
|
+
|
|
47
|
+
reasoning_result = handle_reasoning_key_binding(key)
|
|
48
|
+
return reasoning_result unless reasoning_result == false
|
|
49
|
+
|
|
50
|
+
tab_result = handle_tab_key_binding(key)
|
|
51
|
+
return tab_result unless tab_result == false
|
|
34
52
|
|
|
35
53
|
binding_result = handle_composer_key_binding(key)
|
|
36
54
|
return binding_result unless binding_result == false
|
|
37
55
|
|
|
38
|
-
|
|
39
|
-
case key_name
|
|
56
|
+
case key_name_for(key)
|
|
40
57
|
when :return, :enter
|
|
41
|
-
submit_input
|
|
58
|
+
file_open_overlay_visible? ? open_selected_file_in_editor(fallback_to_typed_path: true) : submit_input
|
|
42
59
|
when :backspace
|
|
43
60
|
delete_before_cursor
|
|
44
61
|
when :delete
|
|
@@ -65,6 +82,8 @@ module Kward
|
|
|
65
82
|
yank_kill_buffer
|
|
66
83
|
when :ctrl_l
|
|
67
84
|
redraw_screen_locked
|
|
85
|
+
when :ctrl_r
|
|
86
|
+
start_history_search
|
|
68
87
|
when :left
|
|
69
88
|
move_cursor_left
|
|
70
89
|
when :right
|
|
@@ -74,21 +93,35 @@ module Kward
|
|
|
74
93
|
when :end
|
|
75
94
|
move_to_end_of_line
|
|
76
95
|
when :up
|
|
77
|
-
|
|
96
|
+
if file_overlay_visible?
|
|
97
|
+
select_previous_file_mention
|
|
98
|
+
elsif slash_overlay_visible?
|
|
99
|
+
select_previous_slash_command
|
|
100
|
+
else
|
|
101
|
+
recall_previous_history
|
|
102
|
+
end
|
|
78
103
|
when :down
|
|
79
|
-
|
|
104
|
+
if file_overlay_visible?
|
|
105
|
+
select_next_file_mention
|
|
106
|
+
elsif slash_overlay_visible?
|
|
107
|
+
select_next_slash_command
|
|
108
|
+
else
|
|
109
|
+
recall_next_history
|
|
110
|
+
end
|
|
80
111
|
else
|
|
81
112
|
case key
|
|
82
113
|
when "\n", "\r"
|
|
83
|
-
submit_input
|
|
114
|
+
file_open_overlay_visible? ? open_selected_file_in_editor(fallback_to_typed_path: true) : submit_input
|
|
84
115
|
when "\t"
|
|
85
|
-
|
|
116
|
+
handle_tab_completion_key
|
|
86
117
|
when "\b", "\x7F"
|
|
87
118
|
delete_before_cursor
|
|
88
119
|
when "\x04"
|
|
89
120
|
delete_at_cursor_or_exit
|
|
90
121
|
when "\x03"
|
|
91
122
|
cancel_input_or_interrupt
|
|
123
|
+
when "\x12"
|
|
124
|
+
start_history_search
|
|
92
125
|
when "\e"
|
|
93
126
|
handle_escape_sequence
|
|
94
127
|
else
|
|
@@ -97,31 +130,114 @@ module Kward
|
|
|
97
130
|
end
|
|
98
131
|
end
|
|
99
132
|
|
|
133
|
+
def handle_history_search_key(key)
|
|
134
|
+
csi_result = handle_history_search_csi_u_key(key)
|
|
135
|
+
return csi_result unless csi_result == false
|
|
136
|
+
return true if handle_bundled_key(key) { |token| handle_history_search_key(token) }
|
|
137
|
+
|
|
138
|
+
case key_name_for(key)
|
|
139
|
+
when :return, :enter
|
|
140
|
+
accept_history_search
|
|
141
|
+
when :up
|
|
142
|
+
select_previous_history_search_match
|
|
143
|
+
when :down
|
|
144
|
+
select_next_history_search_match
|
|
145
|
+
when :backspace
|
|
146
|
+
update_history_search_query(composer_input[0...-1].to_s)
|
|
147
|
+
when :ctrl_c
|
|
148
|
+
cancel_history_search
|
|
149
|
+
else
|
|
150
|
+
case key
|
|
151
|
+
when "\n", "\r"
|
|
152
|
+
accept_history_search
|
|
153
|
+
when "\b", "\x7F"
|
|
154
|
+
update_history_search_query(composer_input[0...-1].to_s)
|
|
155
|
+
when "\x03", "\e"
|
|
156
|
+
cancel_history_search
|
|
157
|
+
else
|
|
158
|
+
append_history_search_key(key)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
true
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def handle_history_search_csi_u_key(key)
|
|
165
|
+
sequence = parse_csi_u_key(key)
|
|
166
|
+
return false unless sequence
|
|
167
|
+
|
|
168
|
+
code = sequence[:code]
|
|
169
|
+
modifier = sequence[:modifier]
|
|
170
|
+
queue_pending_keys(sequence[:remaining]) if sequence[:remaining] && !sequence[:remaining].empty?
|
|
171
|
+
|
|
172
|
+
if ctrl_modifier?(modifier) && ctrl_code_for(code) == 99
|
|
173
|
+
cancel_history_search
|
|
174
|
+
elsif code == 13
|
|
175
|
+
accept_history_search
|
|
176
|
+
elsif code == 27
|
|
177
|
+
cancel_history_search
|
|
178
|
+
elsif code == 8 || code == 127
|
|
179
|
+
update_history_search_query(composer_input[0...-1].to_s)
|
|
180
|
+
else
|
|
181
|
+
text = csi_u_printable_text(sequence)
|
|
182
|
+
update_history_search_query(composer_input + text) if text
|
|
183
|
+
end
|
|
184
|
+
true
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def append_history_search_key(key)
|
|
188
|
+
return unless key.is_a?(String) && key.length == 1 && key.match?(/[[:print:]]/)
|
|
189
|
+
|
|
190
|
+
update_history_search_query(composer_input + key)
|
|
191
|
+
end
|
|
192
|
+
|
|
100
193
|
def cancel_input_or_interrupt
|
|
101
194
|
return CANCEL_INPUT if @busy
|
|
102
195
|
|
|
103
196
|
raise Interrupt
|
|
104
197
|
end
|
|
105
198
|
|
|
199
|
+
def handle_tab_completion_key
|
|
200
|
+
open_selected_file_in_editor || complete_selected_file_mention || complete_selected_slash_command || insert_key("\t")
|
|
201
|
+
end
|
|
202
|
+
|
|
106
203
|
def handle_escape_sequence
|
|
107
204
|
pending_sequence = read_pending_escape_sequence
|
|
108
|
-
return true if pending_sequence.empty? && dismiss_slash_overlay
|
|
205
|
+
return true if pending_sequence.empty? && (dismiss_file_overlay || dismiss_slash_overlay)
|
|
109
206
|
|
|
110
207
|
full_sequence = "\e#{pending_sequence}"
|
|
208
|
+
return true if handle_mouse_reporting_key(full_sequence)
|
|
209
|
+
|
|
111
210
|
sequence = next_key_token(full_sequence)
|
|
112
211
|
queue_pending_keys(full_sequence[sequence.length..]) if full_sequence.length > sequence.length
|
|
113
|
-
return true if sequence == "\e" && dismiss_slash_overlay
|
|
212
|
+
return true if sequence == "\e" && (dismiss_file_overlay || dismiss_slash_overlay)
|
|
114
213
|
return true if handle_shift_enter_key(sequence)
|
|
115
214
|
|
|
215
|
+
reasoning_result = handle_reasoning_key_binding(sequence)
|
|
216
|
+
return reasoning_result unless reasoning_result == false
|
|
217
|
+
|
|
218
|
+
tab_result = handle_tab_key_binding(sequence)
|
|
219
|
+
return tab_result unless tab_result == false
|
|
220
|
+
|
|
116
221
|
binding_result = handle_composer_key_binding(sequence)
|
|
117
222
|
return binding_result unless binding_result == false
|
|
118
223
|
|
|
119
|
-
|
|
120
|
-
case key_name
|
|
224
|
+
case key_name_for(sequence)
|
|
121
225
|
when :up
|
|
122
|
-
|
|
226
|
+
if file_overlay_visible?
|
|
227
|
+
select_previous_file_mention
|
|
228
|
+
elsif slash_overlay_visible?
|
|
229
|
+
select_previous_slash_command
|
|
230
|
+
else
|
|
231
|
+
recall_previous_history
|
|
232
|
+
end
|
|
123
233
|
when :down
|
|
124
|
-
|
|
234
|
+
if file_overlay_visible?
|
|
235
|
+
select_next_file_mention
|
|
236
|
+
elsif slash_overlay_visible?
|
|
237
|
+
select_next_slash_command
|
|
238
|
+
else
|
|
239
|
+
recall_next_history
|
|
240
|
+
end
|
|
125
241
|
when :left
|
|
126
242
|
move_cursor_left
|
|
127
243
|
when :right
|
|
@@ -145,6 +261,15 @@ module Kward
|
|
|
145
261
|
true
|
|
146
262
|
end
|
|
147
263
|
|
|
264
|
+
def handle_mouse_reporting_key(key)
|
|
265
|
+
text = key.to_s
|
|
266
|
+
match = text.match(/\A(?:\e)?\[<\d+;\d+;\d+[Mm]/)
|
|
267
|
+
return false unless match
|
|
268
|
+
|
|
269
|
+
queue_pending_keys(text[match[0].length..]) if match[0].length < text.length
|
|
270
|
+
true
|
|
271
|
+
end
|
|
272
|
+
|
|
148
273
|
def read_bracketed_paste(key)
|
|
149
274
|
text = key.to_s
|
|
150
275
|
return nil unless text.start_with?(BRACKETED_PASTE_START)
|
|
@@ -174,39 +299,93 @@ module Kward
|
|
|
174
299
|
queue_pending_keys(sequence[:remaining]) if sequence[:remaining] && !sequence[:remaining].empty?
|
|
175
300
|
|
|
176
301
|
case code
|
|
302
|
+
when 9
|
|
303
|
+
if ctrl_modifier?(modifier)
|
|
304
|
+
shift_modifier?(modifier) ? { tab_action: :previous } : { tab_action: :next }
|
|
305
|
+
elsif shift_modifier?(modifier)
|
|
306
|
+
handle_reasoning_key_binding(key) || handle_tab_completion_key
|
|
307
|
+
else
|
|
308
|
+
completion_result = handle_completion_provider_key("\t")
|
|
309
|
+
completion_result == false ? handle_reasoning_key_binding("\t") || handle_tab_completion_key : completion_result
|
|
310
|
+
end
|
|
177
311
|
when 13
|
|
178
|
-
modifier == 2
|
|
312
|
+
if modifier == 2
|
|
313
|
+
insert_string("\n")
|
|
314
|
+
elsif file_open_overlay_visible?
|
|
315
|
+
open_selected_file_in_editor(fallback_to_typed_path: true)
|
|
316
|
+
else
|
|
317
|
+
submit_input
|
|
318
|
+
end
|
|
179
319
|
when 27
|
|
180
|
-
dismiss_slash_overlay || false
|
|
320
|
+
dismiss_file_overlay || dismiss_slash_overlay || false
|
|
181
321
|
when 8, 127
|
|
182
322
|
alt_modifier?(modifier) ? delete_word_before_cursor : delete_before_cursor
|
|
183
323
|
nil
|
|
184
324
|
when 4
|
|
185
325
|
delete_at_cursor_or_exit
|
|
186
326
|
else
|
|
187
|
-
handle_modified_csi_u_key(code, modifier)
|
|
327
|
+
handle_modified_csi_u_key(code, modifier) || insert_csi_u_text(sequence)
|
|
188
328
|
end
|
|
189
329
|
end
|
|
190
330
|
|
|
191
331
|
def parse_csi_u_key(key)
|
|
192
|
-
match = key.to_s.match(/\A\e\[(\d+)(?:;
|
|
332
|
+
match = key.to_s.match(/\A\e\[(\d+)((?:;[\d:]*)*)u/)
|
|
193
333
|
return nil unless match
|
|
194
334
|
|
|
195
|
-
|
|
335
|
+
fields = match[2].to_s.split(";", -1)[1..] || []
|
|
336
|
+
modifiers = fields[0].to_s
|
|
196
337
|
modifier = (modifiers.empty? ? "1" : modifiers).split(":", 2).first.to_i
|
|
197
338
|
{
|
|
198
339
|
sequence: match[0],
|
|
199
340
|
code: match[1].to_i,
|
|
200
341
|
modifiers: modifiers,
|
|
201
342
|
modifier: modifier,
|
|
343
|
+
text: fields[1].to_s,
|
|
202
344
|
remaining: key.to_s[match[0].length..]
|
|
203
345
|
}
|
|
204
346
|
end
|
|
205
347
|
|
|
348
|
+
def insert_csi_u_text(sequence)
|
|
349
|
+
text = csi_u_printable_text(sequence)
|
|
350
|
+
return true if text.nil? && csi_u_text_field?(sequence)
|
|
351
|
+
return false unless text
|
|
352
|
+
|
|
353
|
+
insert_string(text)
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def csi_u_text_field?(sequence)
|
|
357
|
+
!sequence[:text].to_s.empty?
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
def csi_u_printable_text(sequence)
|
|
361
|
+
text = csi_u_text(sequence)
|
|
362
|
+
return text unless text.empty?
|
|
363
|
+
return nil if csi_u_text_field?(sequence)
|
|
364
|
+
return nil if ctrl_modifier?(sequence[:modifier]) || alt_modifier?(sequence[:modifier]) || super_modifier?(sequence[:modifier])
|
|
365
|
+
return nil unless sequence[:code].between?(32, 126)
|
|
366
|
+
|
|
367
|
+
sequence[:code].chr(Encoding::UTF_8)
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def csi_u_text(sequence)
|
|
371
|
+
sequence[:text].to_s.split(":").map do |codepoint|
|
|
372
|
+
character = csi_u_codepoint_character(codepoint)
|
|
373
|
+
return "" unless character
|
|
374
|
+
|
|
375
|
+
character
|
|
376
|
+
end.join
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
def csi_u_codepoint_character(codepoint)
|
|
380
|
+
codepoint.to_i.chr(Encoding::UTF_8)
|
|
381
|
+
rescue RangeError
|
|
382
|
+
nil
|
|
383
|
+
end
|
|
384
|
+
|
|
206
385
|
def handle_modified_csi_u_key(code, modifier)
|
|
207
386
|
return false unless ctrl_modifier?(modifier) || alt_modifier?(modifier)
|
|
208
387
|
|
|
209
|
-
normalized_code = code
|
|
388
|
+
normalized_code = ctrl_code_for(code)
|
|
210
389
|
if ctrl_modifier?(modifier)
|
|
211
390
|
case normalized_code
|
|
212
391
|
when 97
|
|
@@ -227,6 +406,8 @@ module Kward
|
|
|
227
406
|
kill_line_after_cursor
|
|
228
407
|
when 108
|
|
229
408
|
redraw_screen_locked
|
|
409
|
+
when 114
|
|
410
|
+
start_history_search
|
|
230
411
|
when 117
|
|
231
412
|
kill_line_before_cursor
|
|
232
413
|
when 119
|
|
@@ -252,6 +433,32 @@ module Kward
|
|
|
252
433
|
end
|
|
253
434
|
end
|
|
254
435
|
|
|
436
|
+
def ctrl_code_for(code)
|
|
437
|
+
code.to_i.chr.downcase.ord rescue code
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
def key_name_for(key)
|
|
441
|
+
cursor_key_name(key) || @reader.console.keys[key]
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
def cursor_key_name(key)
|
|
445
|
+
text = key.to_s
|
|
446
|
+
case text
|
|
447
|
+
when /\A\e\[[0-9;:]*A\z/, "\eOA"
|
|
448
|
+
:up
|
|
449
|
+
when /\A\e\[[0-9;:]*B\z/, "\eOB"
|
|
450
|
+
:down
|
|
451
|
+
when /\A\e\[[0-9;:]*C\z/, "\eOC"
|
|
452
|
+
:right
|
|
453
|
+
when /\A\e\[[0-9;:]*D\z/, "\eOD"
|
|
454
|
+
:left
|
|
455
|
+
when "\e[5~"
|
|
456
|
+
:pageup
|
|
457
|
+
when "\e[6~"
|
|
458
|
+
:pagedown
|
|
459
|
+
end
|
|
460
|
+
end
|
|
461
|
+
|
|
255
462
|
def ctrl_modifier?(modifier)
|
|
256
463
|
((modifier.to_i - 1) & 4).positive?
|
|
257
464
|
end
|
|
@@ -260,6 +467,14 @@ module Kward
|
|
|
260
467
|
((modifier.to_i - 1) & 2).positive?
|
|
261
468
|
end
|
|
262
469
|
|
|
470
|
+
def super_modifier?(modifier)
|
|
471
|
+
((modifier.to_i - 1) & 8).positive?
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
def shift_modifier?(modifier)
|
|
475
|
+
((modifier.to_i - 1) & 1).positive?
|
|
476
|
+
end
|
|
477
|
+
|
|
263
478
|
def handle_shift_enter_key(key)
|
|
264
479
|
sequence = shift_enter_sequence_for(key)
|
|
265
480
|
return false unless sequence
|
|
@@ -278,6 +493,17 @@ module Kward
|
|
|
278
493
|
end
|
|
279
494
|
end
|
|
280
495
|
|
|
496
|
+
def handle_bundled_key(key)
|
|
497
|
+
return false unless key.is_a?(String) && key.length > 1
|
|
498
|
+
|
|
499
|
+
token = next_key_token(key)
|
|
500
|
+
return false unless token.length < key.length
|
|
501
|
+
|
|
502
|
+
queue_pending_keys(key[token.length..])
|
|
503
|
+
yield token
|
|
504
|
+
true
|
|
505
|
+
end
|
|
506
|
+
|
|
281
507
|
def next_key_token(keys)
|
|
282
508
|
text = keys.to_s
|
|
283
509
|
text.match(/\A\e\[[0-9;:]*[A-Za-z~]/)&.[](0) ||
|
|
@@ -288,7 +514,7 @@ module Kward
|
|
|
288
514
|
|
|
289
515
|
def alt_key_sequence?(char)
|
|
290
516
|
char = char.to_s
|
|
291
|
-
char.match?(/[[:
|
|
517
|
+
char.match?(/[[:alnum:]]/) || char == "\b" || char == "\x7F"
|
|
292
518
|
end
|
|
293
519
|
|
|
294
520
|
def shift_enter_sequence_for(key)
|
|
@@ -310,6 +536,123 @@ module Kward
|
|
|
310
536
|
sequence
|
|
311
537
|
end
|
|
312
538
|
|
|
539
|
+
CTRL_TAB_SEQUENCES = ["\e[9;5u", "\e[27;5;9~", "\e[1;5I"].freeze
|
|
540
|
+
CTRL_SHIFT_TAB_SEQUENCES = ["\e[9;6u", "\e[27;6;9~", "\e[1;6I", "\e[1;6Z"].freeze
|
|
541
|
+
SHIFT_TAB_SEQUENCES = ["\e[Z", "\e[1;2Z", "\e[9;2u", "\e[27;2;9~", "\e[1;2I"].freeze
|
|
542
|
+
|
|
543
|
+
def handle_completion_provider_key(key)
|
|
544
|
+
return false unless key == "\t" && @completion_provider
|
|
545
|
+
|
|
546
|
+
result = @completion_provider.call(composer_input, composer_cursor)
|
|
547
|
+
return true unless result
|
|
548
|
+
|
|
549
|
+
apply_completion_result(result)
|
|
550
|
+
true
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
def apply_completion_result(result)
|
|
554
|
+
range = result[:range] || result["range"] || result.range
|
|
555
|
+
replacement = result[:replacement] || result["replacement"] || result.replacement
|
|
556
|
+
candidates = result[:candidates] || result["candidates"] || result.candidates
|
|
557
|
+
original = composer_input
|
|
558
|
+
before = original[0...range.begin].to_s
|
|
559
|
+
after = original[range.end..].to_s
|
|
560
|
+
self.composer_input = "#{before}#{replacement}#{after}"
|
|
561
|
+
self.composer_cursor = before.length + replacement.to_s.length
|
|
562
|
+
show_completion_candidates(candidates, replacement) if candidates.to_a.length > 1 && replacement.to_s == original[range]
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
def show_completion_candidates(candidates, replacement)
|
|
566
|
+
lines = candidates.to_a.first(40)
|
|
567
|
+
text = ["completions:", *lines.map { |candidate| " #{candidate}" }].join("\n")
|
|
568
|
+
write_completion_transcript_locked(text)
|
|
569
|
+
end
|
|
570
|
+
|
|
571
|
+
def write_completion_transcript_locked(text)
|
|
572
|
+
with_synchronized_output_locked do
|
|
573
|
+
clear_prompt_for_output_locked
|
|
574
|
+
write_transcript_text_locked("\n#{text}\n")
|
|
575
|
+
render_prompt_after_output_locked
|
|
576
|
+
end
|
|
577
|
+
end
|
|
578
|
+
|
|
579
|
+
def handle_reasoning_key_binding(key)
|
|
580
|
+
return false if @busy || @select_state || @question_state
|
|
581
|
+
return false if file_overlay_visible? || slash_overlay_visible?
|
|
582
|
+
return false if @slash_overlay_dismissed_input && @slash_overlay_dismissed_input == composer_input
|
|
583
|
+
mention_token = active_file_mention_token
|
|
584
|
+
open_token = active_file_open_token
|
|
585
|
+
return false if mention_token && @file_overlay_dismissed_token == mention_token
|
|
586
|
+
return false if open_token && @file_open_dismissed_token == open_token
|
|
587
|
+
|
|
588
|
+
case key
|
|
589
|
+
when "\t"
|
|
590
|
+
{ reasoning_action: :next }
|
|
591
|
+
when *SHIFT_TAB_SEQUENCES
|
|
592
|
+
{ reasoning_action: :previous }
|
|
593
|
+
else
|
|
594
|
+
false
|
|
595
|
+
end
|
|
596
|
+
end
|
|
597
|
+
|
|
598
|
+
def handle_tab_key_binding(key)
|
|
599
|
+
return false if @select_state || @question_state || @tabs.empty?
|
|
600
|
+
|
|
601
|
+
navigation_result = handle_ctrl_tab_navigation_key_binding(key)
|
|
602
|
+
return navigation_result unless navigation_result == false
|
|
603
|
+
|
|
604
|
+
@tab_keybindings == "ctrl" ? handle_ctrl_tab_key_binding(key) : handle_alt_tab_key_binding(key)
|
|
605
|
+
end
|
|
606
|
+
|
|
607
|
+
def handle_ctrl_tab_navigation_key_binding(key)
|
|
608
|
+
case key
|
|
609
|
+
when *CTRL_TAB_SEQUENCES
|
|
610
|
+
{ tab_action: :next }
|
|
611
|
+
when *CTRL_SHIFT_TAB_SEQUENCES
|
|
612
|
+
{ tab_action: :previous }
|
|
613
|
+
else
|
|
614
|
+
false
|
|
615
|
+
end
|
|
616
|
+
end
|
|
617
|
+
|
|
618
|
+
def handle_ctrl_tab_key_binding(key)
|
|
619
|
+
case key
|
|
620
|
+
when "\x14", "\e[116;5u"
|
|
621
|
+
{ tab_action: :new }
|
|
622
|
+
when "\e[119;5u"
|
|
623
|
+
{ tab_action: :close }
|
|
624
|
+
else
|
|
625
|
+
ctrl_number_tab_action(key)
|
|
626
|
+
end
|
|
627
|
+
end
|
|
628
|
+
|
|
629
|
+
def ctrl_number_tab_action(key)
|
|
630
|
+
match = key.to_s.match(/\A\e\[((?:49)|(?:5[0-7]));5u\z/)
|
|
631
|
+
return false unless match
|
|
632
|
+
|
|
633
|
+
{ tab_action: :select, index: match[1].to_i - 49 }
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
def handle_alt_tab_key_binding(key)
|
|
637
|
+
case key
|
|
638
|
+
when "\et", "\eT"
|
|
639
|
+
{ tab_action: :new }
|
|
640
|
+
when "\e[1;3C", "\e[3C"
|
|
641
|
+
{ tab_action: :next }
|
|
642
|
+
when "\e[1;3D", "\e[3D"
|
|
643
|
+
{ tab_action: :previous }
|
|
644
|
+
else
|
|
645
|
+
alt_number_tab_action(key)
|
|
646
|
+
end
|
|
647
|
+
end
|
|
648
|
+
|
|
649
|
+
def alt_number_tab_action(key)
|
|
650
|
+
match = key.to_s.match(/\A\e([1-9])\z/)
|
|
651
|
+
return false unless match
|
|
652
|
+
|
|
653
|
+
{ tab_action: :select, index: match[1].to_i - 1 }
|
|
654
|
+
end
|
|
655
|
+
|
|
313
656
|
def handle_composer_key_binding(key)
|
|
314
657
|
case key
|
|
315
658
|
when "\x01"
|
|
@@ -355,14 +698,23 @@ module Kward
|
|
|
355
698
|
end
|
|
356
699
|
end
|
|
357
700
|
|
|
701
|
+
def parse_modified_ansi_key(key)
|
|
702
|
+
if (match = key.to_s.match(/\A\e\[(\d+);(\d+)([CDFH])\z/))
|
|
703
|
+
{ type: :cursor, modifier: match[2].to_i, final: match[3] }
|
|
704
|
+
elsif (match = key.to_s.match(/\A\e\[3;(\d+)~\z/))
|
|
705
|
+
{ type: :delete, modifier: match[1].to_i }
|
|
706
|
+
end
|
|
707
|
+
end
|
|
708
|
+
|
|
358
709
|
def handle_modified_ansi_key(key)
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
710
|
+
sequence = parse_modified_ansi_key(key)
|
|
711
|
+
return false unless sequence
|
|
712
|
+
|
|
713
|
+
case sequence[:type]
|
|
714
|
+
when :cursor
|
|
715
|
+
return false unless alt_modifier?(sequence[:modifier])
|
|
364
716
|
|
|
365
|
-
case final
|
|
717
|
+
case sequence[:final]
|
|
366
718
|
when "C"
|
|
367
719
|
move_to_next_word
|
|
368
720
|
when "D"
|
|
@@ -374,8 +726,8 @@ module Kward
|
|
|
374
726
|
else
|
|
375
727
|
false
|
|
376
728
|
end
|
|
377
|
-
|
|
378
|
-
alt_modifier?(
|
|
729
|
+
when :delete
|
|
730
|
+
alt_modifier?(sequence[:modifier]) ? delete_word_after_cursor : delete_at_cursor
|
|
379
731
|
else
|
|
380
732
|
false
|
|
381
733
|
end
|
|
@@ -9,10 +9,29 @@ module Kward
|
|
|
9
9
|
def active_overlay_rows(width, height: screen_height)
|
|
10
10
|
return question_overlay_rows(width) if @question_state
|
|
11
11
|
return selection_overlay_rows(width, height: height) if @select_state
|
|
12
|
+
return git_overlay_rows(width, height: height) if @git_state
|
|
13
|
+
return project_browser_rows(width, height: height) if project_browser_visible?
|
|
14
|
+
return history_search_overlay_rows(width, height: height) if history_search_active?
|
|
15
|
+
return file_overlay_rows(width, height: height) if file_overlay_visible?
|
|
12
16
|
|
|
13
17
|
slash_overlay_rows(width, height: height)
|
|
14
18
|
end
|
|
15
19
|
|
|
20
|
+
def history_search_overlay_rows(width, height: screen_height)
|
|
21
|
+
matches = history_search_matches
|
|
22
|
+
if matches.empty?
|
|
23
|
+
return overlay_card_rows("History", [overlay_text_line("No matching history", :muted)], width)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
max_rows = max_overlay_list_rows(height)
|
|
27
|
+
selected = @composer.history_search_index
|
|
28
|
+
start = centered_list_window_start(selected, matches.length, max_rows)
|
|
29
|
+
rows = (matches[start, max_rows] || []).each_with_index.map do |value, offset|
|
|
30
|
+
overlay_choice_line(value, selected: start + offset == selected)
|
|
31
|
+
end
|
|
32
|
+
overlay_card_rows("History", rows, width)
|
|
33
|
+
end
|
|
34
|
+
|
|
16
35
|
def overlay_card_rows(title, content_rows, width)
|
|
17
36
|
card_width = overlay_card_width(width)
|
|
18
37
|
inner_width = [card_width - 4, 1].max
|
|
@@ -32,7 +51,7 @@ module Kward
|
|
|
32
51
|
def overlay_top_border(title, card_width)
|
|
33
52
|
title = visible_truncate(title.to_s, [card_width - 4, 1].max)
|
|
34
53
|
plain_length = ANSI.strip(title).length
|
|
35
|
-
colored("╭", :primary_green) + " #{colored(title, :
|
|
54
|
+
colored("╭", :primary_green) + " #{colored(title, :primary_green, :bold)} " + colored("─" * [card_width - plain_length - 4, 0].max, :primary_green) + colored("╮", :primary_green)
|
|
36
55
|
end
|
|
37
56
|
|
|
38
57
|
def overlay_bottom_border(card_width)
|
|
@@ -41,7 +60,7 @@ module Kward
|
|
|
41
60
|
|
|
42
61
|
def overlay_content_row(row, inner_width)
|
|
43
62
|
text = visible_truncate(row[:text], inner_width)
|
|
44
|
-
text = colored(text, :
|
|
63
|
+
text = colored(text, :primary_green, :bold) if row[:selected]
|
|
45
64
|
colored("│", :primary_green) + " " + visible_ljust(text, inner_width) + " " + colored("│", :primary_green)
|
|
46
65
|
end
|
|
47
66
|
|