kward 0.82.0 → 0.83.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 +28 -0
- data/Gemfile.lock +2 -2
- data/doc/agent-tools.md +1 -0
- data/doc/composer.md +1 -1
- data/doc/configuration.md +18 -3
- data/doc/editor.md +20 -10
- data/doc/files.md +4 -3
- data/doc/permissions.md +1 -0
- data/doc/releasing.md +12 -4
- data/doc/rpc.md +2 -2
- data/doc/sandboxing.md +5 -1
- data/doc/security.md +1 -1
- data/doc/shell.md +40 -16
- data/doc/tabs.md +3 -0
- data/doc/usage.md +3 -2
- data/lib/kward/agent.rb +1 -0
- data/lib/kward/auth/anthropic_oauth.rb +7 -7
- data/lib/kward/cli/interactive_turn.rb +101 -19
- data/lib/kward/cli/runtime_helpers.rb +156 -14
- data/lib/kward/cli/slash_commands.rb +12 -1
- data/lib/kward/cli/tabs.rb +12 -5
- data/lib/kward/cli.rb +10 -0
- data/lib/kward/compaction/token_estimator.rb +12 -6
- data/lib/kward/config_files.rb +17 -0
- data/lib/kward/editor_prompt.rb +46 -0
- data/lib/kward/editor_prompt_session.rb +28 -0
- data/lib/kward/ekwsh.rb +70 -10
- data/lib/kward/model/client.rb +1 -17
- data/lib/kward/model/model_info.rb +3 -2
- data/lib/kward/model/payloads.rb +0 -2
- data/lib/kward/persistent_shell_session.rb +750 -0
- data/lib/kward/project_files.rb +18 -5
- data/lib/kward/prompt_interface/composer_renderer.rb +1 -1
- data/lib/kward/prompt_interface/editor/auto_indent.rb +3 -0
- data/lib/kward/prompt_interface/editor/controller.rb +69 -0
- data/lib/kward/prompt_interface/editor/modes/modern.rb +4 -0
- data/lib/kward/prompt_interface/editor/modes/vibe.rb +10 -4
- data/lib/kward/prompt_interface/editor/renderer.rb +1 -0
- data/lib/kward/prompt_interface/editor/state.rb +11 -0
- data/lib/kward/prompt_interface/editor/word_completion.rb +124 -0
- data/lib/kward/prompt_interface/file_overlay.rb +21 -7
- data/lib/kward/prompt_interface/key_handler.rb +8 -0
- data/lib/kward/prompt_interface/project_browser.rb +30 -5
- data/lib/kward/prompt_interface/runtime_state.rb +5 -1
- data/lib/kward/prompt_interface.rb +76 -0
- data/lib/kward/prompts/commands.rb +1 -0
- data/lib/kward/rpc/auth_manager.rb +1 -1
- data/lib/kward/rpc/server.rb +2 -1
- data/lib/kward/shell_prompt.rb +50 -0
- data/lib/kward/shell_prompt_session.rb +58 -0
- data/lib/kward/terminal_keys.rb +1 -0
- data/lib/kward/tools/prepare_shell_command.rb +28 -0
- data/lib/kward/tools/registry.rb +63 -5
- data/lib/kward/tools/replace_editor_buffer.rb +30 -0
- data/lib/kward/tools/run_shell_command.rb +24 -6
- data/lib/kward/version.rb +1 -1
- data/templates/default/layout/html/layout.erb +1 -1
- metadata +9 -1
data/lib/kward/project_files.rb
CHANGED
|
@@ -8,21 +8,34 @@ module Kward
|
|
|
8
8
|
module ProjectFiles
|
|
9
9
|
module_function
|
|
10
10
|
|
|
11
|
-
def list(root: Dir.pwd)
|
|
12
|
-
paths = git_paths(root)
|
|
13
|
-
paths = scanned_paths(root) if paths.empty?
|
|
11
|
+
def list(root: Dir.pwd, include_ignored: false)
|
|
12
|
+
paths = git_paths(root, include_ignored: include_ignored)
|
|
13
|
+
paths = scanned_paths(root) if paths.empty? && !git_repository?(root)
|
|
14
14
|
paths.reject { |path| path.empty? || path.end_with?("/") }.uniq.sort
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
-
def git_paths(root)
|
|
17
|
+
def git_paths(root, include_ignored: false)
|
|
18
18
|
output, status = Open3.capture2("git", "ls-files", "--cached", "--others", "--exclude-standard", chdir: root)
|
|
19
19
|
return [] unless status.success?
|
|
20
20
|
|
|
21
|
-
output.lines.map(&:chomp).reject(&:empty?)
|
|
21
|
+
paths = output.lines.map(&:chomp).reject(&:empty?)
|
|
22
|
+
return paths unless include_ignored
|
|
23
|
+
|
|
24
|
+
ignored_output, ignored_status = Open3.capture2("git", "ls-files", "--others", "--ignored", "--exclude-standard", chdir: root)
|
|
25
|
+
return paths unless ignored_status.success?
|
|
26
|
+
|
|
27
|
+
paths + ignored_output.lines.map(&:chomp).reject(&:empty?)
|
|
22
28
|
rescue StandardError
|
|
23
29
|
[]
|
|
24
30
|
end
|
|
25
31
|
|
|
32
|
+
def git_repository?(root)
|
|
33
|
+
_output, status = Open3.capture2e("git", "rev-parse", "--is-inside-work-tree", chdir: root)
|
|
34
|
+
status.success?
|
|
35
|
+
rescue StandardError
|
|
36
|
+
false
|
|
37
|
+
end
|
|
38
|
+
|
|
26
39
|
def scanned_paths(root)
|
|
27
40
|
root_path = Pathname.new(root)
|
|
28
41
|
paths = []
|
|
@@ -8,7 +8,7 @@ module Kward
|
|
|
8
8
|
|
|
9
9
|
def composer_layout(width, height = screen_height)
|
|
10
10
|
return interactive_layout(width, height) if interactive_active_locked?
|
|
11
|
-
return editor_layout(width, height) if
|
|
11
|
+
return editor_layout(width, height) if editor_visible?
|
|
12
12
|
return compact_composer_layout(width) if height < 4
|
|
13
13
|
return question_composer_layout(width, height) if @question_state
|
|
14
14
|
|
|
@@ -83,9 +83,12 @@ module Kward
|
|
|
83
83
|
|
|
84
84
|
def editor_insert_tab
|
|
85
85
|
if @editor_state.multi_cursor? || @editor_state.selection_ranges.any?
|
|
86
|
+
reset_editor_word_completion
|
|
86
87
|
return @editor_state.replace_selections(editor_indent_unit)
|
|
87
88
|
end
|
|
88
89
|
|
|
90
|
+
return true if editor_complete_buffer_word
|
|
91
|
+
|
|
89
92
|
if current_editor_auto_indent? && editor_cursor_in_leading_indent?
|
|
90
93
|
return editor_smart_tab_forward
|
|
91
94
|
end
|
|
@@ -13,6 +13,10 @@ module Kward
|
|
|
13
13
|
!@editor_state.nil?
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
+
def editor_visible?
|
|
17
|
+
editor_active?
|
|
18
|
+
end
|
|
19
|
+
|
|
16
20
|
def open_selected_file_in_editor(fallback_to_typed_path: false)
|
|
17
21
|
path = selected_file_open_path
|
|
18
22
|
if path
|
|
@@ -58,6 +62,7 @@ module Kward
|
|
|
58
62
|
language: language
|
|
59
63
|
)
|
|
60
64
|
@editor_state.status = scratchpad_status_text(language)
|
|
65
|
+
@editor_agent_suspended = false
|
|
61
66
|
@prompt_label = "Edit>"
|
|
62
67
|
self.composer_input = ""
|
|
63
68
|
self.composer_cursor = 0
|
|
@@ -95,6 +100,7 @@ module Kward
|
|
|
95
100
|
end
|
|
96
101
|
|
|
97
102
|
@editor_state = EditorState.new(path: full_path, content: File.exist?(full_path) ? File.read(full_path) : "", new_file: !File.exist?(full_path), editor_mode: current_editor_mode)
|
|
103
|
+
@editor_agent_suspended = false
|
|
98
104
|
@prompt_label = "Edit>"
|
|
99
105
|
self.composer_input = ""
|
|
100
106
|
self.composer_cursor = 0
|
|
@@ -115,6 +121,7 @@ module Kward
|
|
|
115
121
|
diff_view_mode = current_diff_view_mode
|
|
116
122
|
viewer_content = diff_view_mode == DiffViewMode::SIDE_BY_SIDE ? side_by_side_diff_content(content.to_s) : content.to_s
|
|
117
123
|
@editor_state = EditorState.new(path: path.to_s, content: viewer_content, new_file: true, editor_mode: current_editor_mode, readonly: true, diff_view: diff_view_mode)
|
|
124
|
+
@editor_agent_suspended = false
|
|
118
125
|
@prompt_label = "Diff>"
|
|
119
126
|
self.composer_input = ""
|
|
120
127
|
self.composer_cursor = 0
|
|
@@ -243,6 +250,7 @@ module Kward
|
|
|
243
250
|
@editor_save_as_buffer = ""
|
|
244
251
|
@editor_save_callback = nil
|
|
245
252
|
@editor_state = nil
|
|
253
|
+
@editor_agent_suspended = false
|
|
246
254
|
@prompt_label = "You>"
|
|
247
255
|
self.composer_input = ""
|
|
248
256
|
self.composer_cursor = 0
|
|
@@ -252,6 +260,9 @@ module Kward
|
|
|
252
260
|
|
|
253
261
|
def handle_editor_key(key)
|
|
254
262
|
return if key.nil?
|
|
263
|
+
return handle_editor_prompt_key(key) if @editor_prompt_active
|
|
264
|
+
|
|
265
|
+
reset_editor_word_completion unless editor_word_completion_tab_key?(key) && !editor_search_active?
|
|
255
266
|
mouse_result = handle_editor_mouse_key(key)
|
|
256
267
|
return mouse_result unless mouse_result == false
|
|
257
268
|
return handle_editor_save_as_key(key) if @editor_save_as_active
|
|
@@ -312,6 +323,64 @@ module Kward
|
|
|
312
323
|
end
|
|
313
324
|
end
|
|
314
325
|
|
|
326
|
+
def editor_prompt_action(instruction)
|
|
327
|
+
{
|
|
328
|
+
editor_prompt: {
|
|
329
|
+
instruction: instruction.to_s.strip,
|
|
330
|
+
display_input: ":prompt #{instruction.to_s.strip}"
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def begin_editor_prompt
|
|
336
|
+
@editor_prompt_active = true
|
|
337
|
+
@editor_prompt_input = ""
|
|
338
|
+
@editor_state.status = "Prompt: "
|
|
339
|
+
true
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def handle_editor_prompt_key(key)
|
|
343
|
+
sequence = parse_csi_u_key(key)
|
|
344
|
+
if sequence
|
|
345
|
+
queue_pending_keys(sequence[:remaining]) if sequence[:remaining] && !sequence[:remaining].empty?
|
|
346
|
+
return handle_editor_prompt_key("\r") if sequence[:code] == 13
|
|
347
|
+
return handle_editor_prompt_key("\e") if sequence[:code] == 27
|
|
348
|
+
return handle_editor_prompt_key("\b") if [8, 127].include?(sequence[:code])
|
|
349
|
+
|
|
350
|
+
text = csi_u_printable_text(sequence)
|
|
351
|
+
return handle_editor_prompt_key(text) if text
|
|
352
|
+
|
|
353
|
+
return true
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
case key
|
|
357
|
+
when "\e", TerminalKeys::CTRL_C, :escape
|
|
358
|
+
@editor_prompt_active = false
|
|
359
|
+
@editor_prompt_input = ""
|
|
360
|
+
@editor_state.status = EditorStatusText.default(readonly: @editor_state.readonly?, editor_mode: @editor_state.editor_mode)
|
|
361
|
+
true
|
|
362
|
+
when "\b", "\x7F"
|
|
363
|
+
@editor_prompt_input = @editor_prompt_input[0...-1]
|
|
364
|
+
@editor_state.status = "Prompt: #{@editor_prompt_input}"
|
|
365
|
+
when "\n", "\r"
|
|
366
|
+
instruction = @editor_prompt_input.strip
|
|
367
|
+
@editor_prompt_active = false
|
|
368
|
+
@editor_prompt_input = ""
|
|
369
|
+
if instruction.empty?
|
|
370
|
+
@editor_state.status = "Prompt instruction required"
|
|
371
|
+
true
|
|
372
|
+
else
|
|
373
|
+
editor_prompt_action(instruction)
|
|
374
|
+
end
|
|
375
|
+
else
|
|
376
|
+
if printable_key?(key)
|
|
377
|
+
@editor_prompt_input += key
|
|
378
|
+
@editor_state.status = "Prompt: #{@editor_prompt_input}"
|
|
379
|
+
end
|
|
380
|
+
true
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
|
|
315
384
|
def handle_editor_csi_u_key(key)
|
|
316
385
|
sequence = parse_csi_u_key(key)
|
|
317
386
|
return false unless sequence
|
|
@@ -198,6 +198,8 @@ module Kward
|
|
|
198
198
|
|
|
199
199
|
def handle_modern_key_binding(key)
|
|
200
200
|
case key
|
|
201
|
+
when TerminalKeys::CTRL_PERIOD
|
|
202
|
+
begin_editor_prompt
|
|
201
203
|
when TerminalKeys::CTRL_SPACE
|
|
202
204
|
true
|
|
203
205
|
when TerminalKeys::CTRL_C
|
|
@@ -221,6 +223,8 @@ module Kward
|
|
|
221
223
|
end
|
|
222
224
|
|
|
223
225
|
case normalized_code
|
|
226
|
+
when 46
|
|
227
|
+
begin_editor_prompt
|
|
224
228
|
when 13
|
|
225
229
|
return false if editor_search_active?
|
|
226
230
|
|
|
@@ -192,6 +192,7 @@ module Kward
|
|
|
192
192
|
end
|
|
193
193
|
|
|
194
194
|
def handle_vibe_command_key(key)
|
|
195
|
+
result = nil
|
|
195
196
|
case key
|
|
196
197
|
when "\e", TerminalKeys::CTRL_C, :escape
|
|
197
198
|
@editor_state.vibe_command = ""
|
|
@@ -202,20 +203,21 @@ module Kward
|
|
|
202
203
|
when "\t"
|
|
203
204
|
vibe_complete_command_path
|
|
204
205
|
when "\n", "\r"
|
|
205
|
-
execute_vibe_command(@editor_state.vibe_command)
|
|
206
|
+
result = execute_vibe_command(@editor_state.vibe_command)
|
|
206
207
|
else
|
|
207
208
|
if printable_key?(key)
|
|
208
209
|
@editor_state.vibe_command = @editor_state.vibe_command.to_s + key
|
|
209
210
|
@editor_state.status = ":#{@editor_state.vibe_command}"
|
|
210
211
|
end
|
|
211
212
|
end
|
|
212
|
-
true
|
|
213
|
+
result || true
|
|
213
214
|
end
|
|
214
215
|
|
|
215
216
|
def execute_vibe_command(command)
|
|
216
217
|
command = command.to_s.strip
|
|
217
218
|
@editor_state.vibe_mode = "normal"
|
|
218
219
|
@editor_state.vibe_command = ""
|
|
220
|
+
action = nil
|
|
219
221
|
case command
|
|
220
222
|
when "w"
|
|
221
223
|
save_editor
|
|
@@ -225,6 +227,10 @@ module Kward
|
|
|
225
227
|
vibe_edit_file(Regexp.last_match(2), force: Regexp.last_match(1) == "!")
|
|
226
228
|
when "run"
|
|
227
229
|
vibe_record_undo { run_editor_buffer }
|
|
230
|
+
when /\Aprompt\s+(.+)\z/
|
|
231
|
+
action = editor_prompt_action(Regexp.last_match(1))
|
|
232
|
+
when "prompt"
|
|
233
|
+
@editor_state.status = "Prompt instruction required"
|
|
228
234
|
when "q"
|
|
229
235
|
vibe_quit_editor
|
|
230
236
|
when "q!"
|
|
@@ -244,7 +250,7 @@ module Kward
|
|
|
244
250
|
else
|
|
245
251
|
@editor_state.status = "Unknown command: #{command}"
|
|
246
252
|
end
|
|
247
|
-
true
|
|
253
|
+
action || true
|
|
248
254
|
end
|
|
249
255
|
|
|
250
256
|
def vibe_edit_file(path, force: false)
|
|
@@ -426,7 +432,7 @@ module Kward
|
|
|
426
432
|
def vibe_return_to_normal
|
|
427
433
|
vibe_apply_visual_block_insert if @editor_state.vibe_visual_block_insert
|
|
428
434
|
@editor_state.vibe_mode = "normal"
|
|
429
|
-
@editor_state.status = "NORMAL · i insert · :w save · :q quit"
|
|
435
|
+
@editor_state.status = "NORMAL · i insert · :prompt ask agent · :w save · :q quit"
|
|
430
436
|
true
|
|
431
437
|
end
|
|
432
438
|
|
|
@@ -264,6 +264,7 @@ module Kward
|
|
|
264
264
|
|
|
265
265
|
def editor_status_text
|
|
266
266
|
text = @editor_state.search_active ? "#{@editor_state.search_direction == :backward ? "Search backward" : "Search"}: #{@editor_state.search_query}" : @editor_state.status
|
|
267
|
+
text = "#{text} · #{spinner_frame}" if @editor_agent_suspended && @busy
|
|
267
268
|
visible_truncate(text, [screen_width - 4, 1].max)
|
|
268
269
|
end
|
|
269
270
|
end
|
|
@@ -830,6 +830,17 @@ module Kward
|
|
|
830
830
|
@cursor = @buffer.length
|
|
831
831
|
end
|
|
832
832
|
|
|
833
|
+
def replace_buffer(content)
|
|
834
|
+
content = content.to_s
|
|
835
|
+
return false if content == @buffer
|
|
836
|
+
|
|
837
|
+
push_undo
|
|
838
|
+
self.buffer = content
|
|
839
|
+
@cursor = [@cursor, @buffer.length].min
|
|
840
|
+
changed!
|
|
841
|
+
true
|
|
842
|
+
end
|
|
843
|
+
|
|
833
844
|
def replace_range(start_index, end_index, text)
|
|
834
845
|
start_index, = @text_buffer.replace_range(start_index, end_index, text)
|
|
835
846
|
@buffer = @text_buffer.text
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# Namespace for the Kward CLI agent runtime.
|
|
2
|
+
module Kward
|
|
3
|
+
# Interactive terminal UI used by the CLI frontend.
|
|
4
|
+
class PromptInterface
|
|
5
|
+
# Current-buffer word completion for the built-in composer file editor.
|
|
6
|
+
module EditorWordCompletion
|
|
7
|
+
EDITOR_COMPLETION_WORD_PATTERN = /[[:alnum:]_]+/.freeze
|
|
8
|
+
EDITOR_COMPLETION_PREFIX_PATTERN = /[[:alnum:]_]+\z/.freeze
|
|
9
|
+
EDITOR_COMPLETION_SUFFIX_PATTERN = /\A[[:alnum:]_]*/.freeze
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def editor_complete_buffer_word
|
|
14
|
+
if (cycle = matching_editor_completion_cycle)
|
|
15
|
+
return cycle_editor_word_completion(cycle) if cycle[:candidates].length > 1
|
|
16
|
+
|
|
17
|
+
reset_editor_word_completion
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
context = editor_word_completion_context
|
|
21
|
+
return false unless context
|
|
22
|
+
|
|
23
|
+
candidates = editor_word_completion_candidates(context)
|
|
24
|
+
return false if candidates.empty?
|
|
25
|
+
|
|
26
|
+
@editor_word_completion = context.merge(
|
|
27
|
+
editor_state: @editor_state,
|
|
28
|
+
candidates: candidates,
|
|
29
|
+
index: 0,
|
|
30
|
+
previous_status: @editor_state.status
|
|
31
|
+
)
|
|
32
|
+
apply_editor_word_completion(@editor_word_completion)
|
|
33
|
+
true
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def editor_word_completion_context
|
|
37
|
+
buffer = @editor_state.buffer
|
|
38
|
+
cursor = @editor_state.cursor
|
|
39
|
+
prefix_match = buffer[0...cursor].to_s.match(EDITOR_COMPLETION_PREFIX_PATTERN)
|
|
40
|
+
return nil unless prefix_match
|
|
41
|
+
|
|
42
|
+
suffix = buffer[cursor..].to_s.match(EDITOR_COMPLETION_SUFFIX_PATTERN).to_s
|
|
43
|
+
{
|
|
44
|
+
prefix: prefix_match[0],
|
|
45
|
+
range_start: prefix_match.begin(0),
|
|
46
|
+
range_end: cursor + suffix.length
|
|
47
|
+
}
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def editor_word_completion_candidates(context)
|
|
51
|
+
matches = []
|
|
52
|
+
@editor_state.buffer.to_enum(:scan, EDITOR_COMPLETION_WORD_PATTERN).each do
|
|
53
|
+
match = Regexp.last_match
|
|
54
|
+
next if editor_completion_match_overlaps_context?(match, context)
|
|
55
|
+
|
|
56
|
+
word = match[0]
|
|
57
|
+
next unless word.length > context[:prefix].length
|
|
58
|
+
next unless word.start_with?(context[:prefix])
|
|
59
|
+
|
|
60
|
+
matches << [word, match.begin(0)]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
matches
|
|
64
|
+
.sort_by { |_word, offset| editor_completion_candidate_order(offset, context) }
|
|
65
|
+
.map(&:first)
|
|
66
|
+
.uniq
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def editor_completion_match_overlaps_context?(match, context)
|
|
70
|
+
match.begin(0) < context[:range_end] && match.end(0) > context[:range_start]
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def editor_completion_candidate_order(offset, context)
|
|
74
|
+
if offset < context[:range_start]
|
|
75
|
+
[0, context[:range_start] - offset]
|
|
76
|
+
else
|
|
77
|
+
[1, offset - context[:range_end]]
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def matching_editor_completion_cycle
|
|
82
|
+
cycle = @editor_word_completion
|
|
83
|
+
return nil unless cycle
|
|
84
|
+
|
|
85
|
+
same_editor = cycle[:editor_state].equal?(@editor_state)
|
|
86
|
+
same_buffer = cycle[:buffer] == @editor_state.buffer
|
|
87
|
+
same_cursor = cycle[:cursor] == @editor_state.cursor
|
|
88
|
+
return cycle if same_editor && same_buffer && same_cursor
|
|
89
|
+
|
|
90
|
+
reset_editor_word_completion
|
|
91
|
+
nil
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def cycle_editor_word_completion(cycle)
|
|
95
|
+
cycle[:index] = (cycle[:index] + 1) % cycle[:candidates].length
|
|
96
|
+
apply_editor_word_completion(cycle)
|
|
97
|
+
true
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def apply_editor_word_completion(cycle)
|
|
101
|
+
candidate = cycle[:candidates][cycle[:index]]
|
|
102
|
+
@editor_state.replace_range(cycle[:range_start], cycle[:range_end], candidate)
|
|
103
|
+
@editor_state.cursor = cycle[:range_start] + candidate.length
|
|
104
|
+
cycle[:range_end] = @editor_state.cursor
|
|
105
|
+
cycle[:buffer] = @editor_state.buffer
|
|
106
|
+
cycle[:cursor] = @editor_state.cursor
|
|
107
|
+
cycle[:status] = "Completion #{cycle[:index] + 1}/#{cycle[:candidates].length}: #{candidate}"
|
|
108
|
+
@editor_state.status = cycle[:status]
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def reset_editor_word_completion
|
|
112
|
+
cycle = @editor_word_completion
|
|
113
|
+
if cycle && cycle[:editor_state].status == cycle[:status]
|
|
114
|
+
cycle[:editor_state].status = cycle[:previous_status]
|
|
115
|
+
end
|
|
116
|
+
@editor_word_completion = nil
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def editor_word_completion_tab_key?(key)
|
|
120
|
+
!editor_tab_sequence_for(key).nil?
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
@@ -114,8 +114,12 @@ module Kward
|
|
|
114
114
|
end
|
|
115
115
|
end
|
|
116
116
|
|
|
117
|
-
def project_file_paths
|
|
118
|
-
|
|
117
|
+
def project_file_paths(include_ignored: false)
|
|
118
|
+
if include_ignored
|
|
119
|
+
@project_browser_file_paths ||= discover_project_file_paths(include_ignored: true)
|
|
120
|
+
else
|
|
121
|
+
@file_mention_paths ||= discover_project_file_paths
|
|
122
|
+
end
|
|
119
123
|
end
|
|
120
124
|
|
|
121
125
|
def project_file_path_entries
|
|
@@ -126,14 +130,24 @@ module Kward
|
|
|
126
130
|
@file_mention_path_entries = paths.map { |path| { path: path, downcase: path.downcase } }
|
|
127
131
|
end
|
|
128
132
|
|
|
129
|
-
def discover_project_file_paths
|
|
130
|
-
paths =
|
|
131
|
-
|
|
133
|
+
def discover_project_file_paths(include_ignored: false)
|
|
134
|
+
paths = if include_ignored
|
|
135
|
+
git_project_file_paths(include_ignored: true)
|
|
136
|
+
else
|
|
137
|
+
git_project_file_paths
|
|
138
|
+
end
|
|
139
|
+
paths = scanned_project_file_paths if paths.empty? && !git_project_repository?
|
|
132
140
|
paths.reject { |path| path.empty? || path.end_with?("/") }.uniq.sort
|
|
133
141
|
end
|
|
134
142
|
|
|
135
|
-
def git_project_file_paths
|
|
136
|
-
ProjectFiles.git_paths(prompt_workspace_root)
|
|
143
|
+
def git_project_file_paths(include_ignored: false)
|
|
144
|
+
return ProjectFiles.git_paths(prompt_workspace_root) unless include_ignored
|
|
145
|
+
|
|
146
|
+
ProjectFiles.git_paths(prompt_workspace_root, include_ignored: true)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def git_project_repository?
|
|
150
|
+
ProjectFiles.git_repository?(prompt_workspace_root)
|
|
137
151
|
end
|
|
138
152
|
|
|
139
153
|
def scanned_project_file_paths
|
|
@@ -20,6 +20,14 @@ module Kward
|
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
def handle_editor_input_key(key)
|
|
23
|
+
if @editor_agent_suspended
|
|
24
|
+
return CANCEL_INPUT if @busy && key == TerminalKeys::CTRL_C
|
|
25
|
+
|
|
26
|
+
return true
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
reset_editor_word_completion unless editor_word_completion_tab_key?(key)
|
|
30
|
+
|
|
23
31
|
tab_result = handle_tab_key_binding(key)
|
|
24
32
|
return tab_result unless tab_result == false
|
|
25
33
|
|
|
@@ -48,10 +48,12 @@ module Kward
|
|
|
48
48
|
end
|
|
49
49
|
|
|
50
50
|
def open_project_browser_locked
|
|
51
|
-
paths = project_file_paths
|
|
52
51
|
saved_state = saved_project_browser_state
|
|
52
|
+
show_ignored = saved_state["show_ignored"] == true
|
|
53
|
+
paths = project_file_paths(include_ignored: show_ignored)
|
|
53
54
|
@project_browser_state = {
|
|
54
55
|
paths: paths,
|
|
56
|
+
show_ignored: show_ignored,
|
|
55
57
|
expanded: restored_project_browser_expanded_paths(paths, saved_state),
|
|
56
58
|
selection_index: 0,
|
|
57
59
|
search_active: false,
|
|
@@ -142,6 +144,8 @@ module Kward
|
|
|
142
144
|
collapse_selected_project_browser_row
|
|
143
145
|
elsif text == "l" && !project_browser_search_active?
|
|
144
146
|
expand_selected_project_browser_row
|
|
147
|
+
elsif text == "i" && !project_browser_search_active?
|
|
148
|
+
toggle_project_browser_ignored_files
|
|
145
149
|
elsif project_browser_search_active?
|
|
146
150
|
project_browser_append_search(text)
|
|
147
151
|
else
|
|
@@ -173,6 +177,8 @@ module Kward
|
|
|
173
177
|
project_browser_search_active? ? project_browser_append_search(key) : collapse_selected_project_browser_row
|
|
174
178
|
when "l"
|
|
175
179
|
project_browser_search_active? ? project_browser_append_search(key) : expand_selected_project_browser_row
|
|
180
|
+
when "i"
|
|
181
|
+
project_browser_search_active? ? project_browser_append_search(key) : toggle_project_browser_ignored_files
|
|
176
182
|
else
|
|
177
183
|
project_browser_append_search(key) if project_browser_search_active? && printable_key?(key)
|
|
178
184
|
end
|
|
@@ -190,6 +196,21 @@ module Kward
|
|
|
190
196
|
project_browser_search_active? ? deactivate_project_browser_search : activate_project_browser_search
|
|
191
197
|
end
|
|
192
198
|
|
|
199
|
+
def toggle_project_browser_ignored_files
|
|
200
|
+
selected_path = selected_project_browser_row&.[](:path)
|
|
201
|
+
show_ignored = !project_browser_ignored_files_visible?
|
|
202
|
+
@project_browser_state[:paths] = project_file_paths(include_ignored: show_ignored)
|
|
203
|
+
@project_browser_state[:show_ignored] = show_ignored
|
|
204
|
+
@project_browser_tree_paths = nil
|
|
205
|
+
@project_browser_tree = nil
|
|
206
|
+
restore_project_browser_selection(selected_path)
|
|
207
|
+
persist_project_browser_state
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def project_browser_ignored_files_visible?
|
|
211
|
+
@project_browser_state[:show_ignored] == true
|
|
212
|
+
end
|
|
213
|
+
|
|
193
214
|
def activate_project_browser_search
|
|
194
215
|
@project_browser_state[:search_active] = true
|
|
195
216
|
@project_browser_state[:query] = ""
|
|
@@ -468,10 +489,12 @@ module Kward
|
|
|
468
489
|
|
|
469
490
|
@project_browser_restore_after_editor = false
|
|
470
491
|
unless @project_browser_state
|
|
471
|
-
paths = project_file_paths
|
|
472
492
|
saved_state = saved_project_browser_state
|
|
493
|
+
show_ignored = saved_state["show_ignored"] == true
|
|
494
|
+
paths = project_file_paths(include_ignored: show_ignored)
|
|
473
495
|
@project_browser_state = {
|
|
474
496
|
paths: paths,
|
|
497
|
+
show_ignored: show_ignored,
|
|
475
498
|
expanded: restored_project_browser_expanded_paths(paths, saved_state),
|
|
476
499
|
selection_index: 0,
|
|
477
500
|
search_active: false,
|
|
@@ -507,7 +530,8 @@ module Kward
|
|
|
507
530
|
|
|
508
531
|
def project_browser_title
|
|
509
532
|
query = @project_browser_state[:query].to_s
|
|
510
|
-
project_browser_search_active? ? "Project files — Search: #{query}" : "Project files"
|
|
533
|
+
title = project_browser_search_active? ? "Project files — Search: #{query}" : "Project files"
|
|
534
|
+
project_browser_ignored_files_visible? ? "#{title} (including ignored)" : title
|
|
511
535
|
end
|
|
512
536
|
|
|
513
537
|
def project_browser_empty_message
|
|
@@ -518,7 +542,7 @@ module Kward
|
|
|
518
542
|
if project_browser_search_active?
|
|
519
543
|
"Type search • Esc tree • Enter open • @ mention"
|
|
520
544
|
else
|
|
521
|
-
"Enter open/toggle • ←/→ collapse/expand • Tab or / search • @ mention • Esc close"
|
|
545
|
+
"Enter open/toggle • i show/hide ignored • ←/→ collapse/expand • Tab or / search • @ mention • Esc close"
|
|
522
546
|
end
|
|
523
547
|
end
|
|
524
548
|
|
|
@@ -631,7 +655,8 @@ module Kward
|
|
|
631
655
|
row = selected_project_browser_row
|
|
632
656
|
workspaces[project_browser_workspace_root] = {
|
|
633
657
|
"expanded" => @project_browser_state[:expanded].to_a.sort,
|
|
634
|
-
"selected_path" => row && row[:path]
|
|
658
|
+
"selected_path" => row && row[:path],
|
|
659
|
+
"show_ignored" => project_browser_ignored_files_visible?
|
|
635
660
|
}
|
|
636
661
|
data["version"] = PROJECT_BROWSER_STATE_VERSION
|
|
637
662
|
data["workspaces"] = workspaces
|
|
@@ -118,8 +118,12 @@ module Kward
|
|
|
118
118
|
result.is_a?(Hash) && result[:reasoning_action]
|
|
119
119
|
end
|
|
120
120
|
|
|
121
|
+
def editor_prompt_action?(result)
|
|
122
|
+
result.is_a?(Hash) && result[:editor_prompt]
|
|
123
|
+
end
|
|
124
|
+
|
|
121
125
|
def prompt_action_result?(result)
|
|
122
|
-
tab_action_result?(result) || reasoning_action_result?(result)
|
|
126
|
+
tab_action_result?(result) || reasoning_action_result?(result) || editor_prompt_action?(result)
|
|
123
127
|
end
|
|
124
128
|
|
|
125
129
|
end
|