kward 0.83.0 → 0.85.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 +102 -15
- data/CONTRIBUTING.md +74 -0
- data/Gemfile.lock +8 -2
- data/README.md +21 -1
- data/Rakefile +46 -2
- data/SECURITY.md +31 -0
- data/doc/agent-tools.md +13 -1
- data/doc/api.md +21 -2
- data/doc/composer.md +2 -2
- data/doc/configuration.md +95 -25
- data/doc/editor.md +28 -13
- data/doc/extensibility.md +2 -1
- data/doc/files.md +8 -4
- data/doc/getting-started.md +3 -0
- data/doc/git.md +3 -1
- data/doc/pan.md +25 -15
- data/doc/permissions.md +4 -4
- data/doc/platform-support.md +48 -0
- data/doc/plugins.md +464 -15
- data/doc/rpc.md +154 -16
- data/doc/sandboxing.md +11 -5
- data/doc/security.md +10 -3
- data/doc/session-management.md +5 -4
- data/doc/shell.md +62 -45
- data/doc/tabs.md +6 -2
- data/doc/transports.md +15 -0
- data/doc/troubleshooting.md +12 -2
- data/doc/usage.md +9 -6
- data/doc/workspace-tools.md +9 -0
- data/examples/plugins/space_invaders.rb +1 -1
- data/examples/plugins/stardate_footer.rb +2 -2
- data/examples/plugins/telegram/plugin.rb +1 -1
- data/kward.gemspec +5 -4
- data/lib/kward/agent.rb +30 -14
- data/lib/kward/cli/auth_commands.rb +34 -13
- data/lib/kward/cli/commands.rb +83 -62
- data/lib/kward/cli/compaction.rb +9 -3
- data/lib/kward/cli/doctor.rb +39 -17
- data/lib/kward/cli/hook_commands.rb +22 -12
- data/lib/kward/cli/interactive_turn.rb +48 -7
- data/lib/kward/cli/plugins.rb +81 -12
- data/lib/kward/cli/project_skills_commands.rb +8 -4
- data/lib/kward/cli/prompt_interface.rb +52 -5
- data/lib/kward/cli/rendering.rb +18 -9
- data/lib/kward/cli/runtime_helpers.rb +228 -71
- data/lib/kward/cli/sessions.rb +9 -5
- data/lib/kward/cli/settings/menus.rb +745 -0
- data/lib/kward/cli/settings/model.rb +327 -0
- data/lib/kward/cli/settings.rb +6 -1055
- data/lib/kward/cli/slash_commands.rb +56 -17
- data/lib/kward/cli/tabs.rb +244 -33
- data/lib/kward/cli/tool_summaries.rb +14 -0
- data/lib/kward/{cli_transcript_formatter.rb → cli/transcript_formatter.rb} +14 -7
- data/lib/kward/cli/worktrees.rb +65 -2
- data/lib/kward/cli.rb +70 -30
- data/lib/kward/compactor.rb +18 -7
- data/lib/kward/config/core.rb +389 -0
- data/lib/kward/config/extensions.rb +96 -0
- data/lib/kward/config/prompts.rb +313 -0
- data/lib/kward/config/settings.rb +250 -0
- data/lib/kward/config_files.rb +14 -994
- data/lib/kward/conversation.rb +31 -2
- data/lib/kward/image_attachments.rb +1 -1
- data/lib/kward/model/client.rb +36 -24
- data/lib/kward/model/copilot_models.rb +2 -2
- data/lib/kward/model/model_info.rb +20 -3
- data/lib/kward/{openrouter_model_cache.rb → model/openrouter_model_cache.rb} +3 -3
- data/lib/kward/model/payloads.rb +12 -3
- data/lib/kward/model/provider_catalog.rb +5 -0
- data/lib/kward/model/stream_parser.rb +20 -4
- data/lib/kward/model/typesafe_client.rb +78 -0
- data/lib/kward/pan/index.html.erb +3 -3
- data/lib/kward/pan/server.rb +33 -10
- data/lib/kward/permissions/policy.rb +6 -2
- data/lib/kward/plugin_registry.rb +2 -659
- data/lib/kward/plugins/actions.rb +453 -0
- data/lib/kward/plugins/chat_contract.rb +121 -0
- data/lib/kward/{plugin_chat_runtime.rb → plugins/chat_runtime.rb} +62 -18
- data/lib/kward/plugins/host.rb +232 -0
- data/lib/kward/plugins/registry.rb +1190 -0
- data/lib/kward/plugins/resources.rb +206 -0
- data/lib/kward/plugins/turn_request.rb +36 -0
- data/lib/kward/plugins/ui.rb +219 -0
- data/lib/kward/prompt_interface/composer_renderer.rb +44 -40
- data/lib/kward/prompt_interface/composer_state.rb +33 -24
- data/lib/kward/prompt_interface/editor/auto_indent.rb +24 -22
- data/lib/kward/prompt_interface/editor/controller.rb +30 -33
- data/lib/kward/prompt_interface/editor/endwise.rb +13 -4
- data/lib/kward/prompt_interface/editor/markdown_code_block.rb +136 -0
- data/lib/kward/prompt_interface/editor/modes/vibe.rb +289 -44
- data/lib/kward/prompt_interface/editor/renderer.rb +108 -6
- data/lib/kward/prompt_interface/editor/runner.rb +362 -0
- data/lib/kward/prompt_interface/editor/runner_state.rb +78 -0
- data/lib/kward/prompt_interface/editor/scratchpad_languages.rb +74 -0
- data/lib/kward/prompt_interface/editor/scratchpad_runner.rb +182 -0
- data/lib/kward/prompt_interface/editor/state.rb +11 -11
- data/lib/kward/prompt_interface/editor/syntax_highlighter.rb +68 -6
- data/lib/kward/prompt_interface/editor/vibe_state.rb +3 -3
- data/lib/kward/prompt_interface/file_overlay.rb +71 -15
- data/lib/kward/prompt_interface/key_handler.rb +67 -0
- data/lib/kward/prompt_interface/layout.rb +1 -1
- data/lib/kward/prompt_interface/overlay_renderer.rb +7 -5
- data/lib/kward/prompt_interface/plugin_ui_requests.rb +82 -0
- data/lib/kward/prompt_interface/project_browser.rb +415 -14
- data/lib/kward/prompt_interface/runtime_state.rb +56 -2
- data/lib/kward/prompt_interface/screen.rb +11 -4
- data/lib/kward/prompt_interface/selection_prompt.rb +3 -1
- data/lib/kward/prompt_interface/slash_overlay.rb +19 -4
- data/lib/kward/prompt_interface/transcript_renderer.rb +12 -7
- data/lib/kward/prompt_interface.rb +151 -27
- data/lib/kward/prompts/commands.rb +3 -2
- data/lib/kward/prompts.rb +1 -1
- data/lib/kward/{adaptive_pty_output_sink.rb → pty/adaptive_output_sink.rb} +1 -1
- data/lib/kward/pty/detached_run.rb +44 -0
- data/lib/kward/{interactive_pty_runner.rb → pty/interactive_runner.rb} +104 -30
- data/lib/kward/{local_command_runner.rb → pty/local_command_runner.rb} +1 -1
- data/lib/kward/{local_pty_command_runner.rb → pty/local_pty_runner.rb} +3 -9
- data/lib/kward/{pty_output_sink.rb → pty/output_sink.rb} +52 -0
- data/lib/kward/{pty_transcript_normalizer.rb → pty/transcript_normalizer.rb} +1 -1
- data/lib/kward/rpc/plugin_chat_manager.rb +30 -10
- data/lib/kward/rpc/prompt_bridge.rb +25 -0
- data/lib/kward/rpc/server.rb +91 -12
- data/lib/kward/rpc/session_manager.rb +147 -44
- data/lib/kward/rpc/session_tree_rows.rb +2 -2
- data/lib/kward/rpc/tool_metadata.rb +1 -1
- data/lib/kward/rpc/transcript_normalizer.rb +7 -3
- data/lib/kward/sandbox/command_runner.rb +1 -1
- data/lib/kward/{session_catalog.rb → sessions/catalog.rb} +1 -1
- data/lib/kward/{session_store.rb → sessions/store.rb} +9 -9
- data/lib/kward/{session_tree_nodes.rb → sessions/tree_nodes.rb} +3 -3
- data/lib/kward/{session_tree_renderer.rb → sessions/tree_renderer.rb} +4 -4
- data/lib/kward/{session_tree_tool_display.rb → sessions/tree_tool_display.rb} +1 -1
- data/lib/kward/{ekwsh.rb → shell/kwsh.rb} +38 -19
- data/lib/kward/shell/kwshrc.rb +233 -0
- data/lib/kward/{persistent_shell_session.rb → shell/persistent_session.rb} +121 -28
- data/lib/kward/{shell_prompt.rb → shell/prompt.rb} +2 -0
- data/lib/kward/{shell_prompt_session.rb → shell/prompt_session.rb} +1 -1
- data/lib/kward/skills/trust_store.rb +1 -1
- data/lib/kward/tabs/driver.rb +194 -0
- data/lib/kward/{tab_store.rb → tabs/store.rb} +2 -2
- data/lib/kward/{ansi.rb → terminal/ansi.rb} +110 -10
- data/lib/kward/{clipboard.rb → terminal/clipboard.rb} +1 -1
- data/lib/kward/{terminal_image_support.rb → terminal/image_support.rb} +1 -1
- data/lib/kward/{terminal_keys.rb → terminal/keys.rb} +12 -0
- data/lib/kward/terminal/text.rb +121 -0
- data/lib/kward/text_matcher.rb +18 -0
- data/lib/kward/tools/base.rb +18 -0
- data/lib/kward/tools/context_for_task.rb +15 -6
- data/lib/kward/tools/edit_file.rb +9 -6
- data/lib/kward/tools/git_commit.rb +13 -7
- data/lib/kward/tools/list_directory.rb +4 -4
- data/lib/kward/tools/open_editor.rb +41 -0
- data/lib/kward/tools/plugin_tool.rb +41 -0
- data/lib/kward/tools/prepare_shell_command.rb +1 -1
- data/lib/kward/tools/read_file.rb +7 -6
- data/lib/kward/tools/registry.rb +109 -17
- data/lib/kward/tools/run_shell_command.rb +10 -8
- data/lib/kward/tools/search/code.rb +1 -1
- data/lib/kward/tools/summarize_file_structure.rb +5 -5
- data/lib/kward/tools/tool_call.rb +3 -1
- data/lib/kward/tools/typesafe_evaluate.rb +81 -0
- data/lib/kward/tools/workspace_targets.rb +58 -0
- data/lib/kward/tools/write_file.rb +9 -6
- data/lib/kward/{export_path.rb → transcripts/export_path.rb} +1 -1
- data/lib/kward/{markdown_transcript.rb → transcripts/markdown_transcript.rb} +2 -2
- data/lib/kward/transport/contracts.rb +200 -0
- data/lib/kward/transport/gateway.rb +79 -35
- data/lib/kward/transport/plugin_chat_gateway.rb +3 -2
- data/lib/kward/transport.rb +1 -200
- data/lib/kward/version.rb +1 -1
- data/lib/kward/{workspace_factory.rb → workspace/factory.rb} +2 -2
- data/lib/kward/{project_files.rb → workspace/files.rb} +2 -2
- data/lib/kward/{git_worktree_manager.rb → workspace/git_worktree_manager.rb} +28 -0
- data/lib/kward/{workspace.rb → workspace/workspace.rb} +3 -3
- data/templates/default/kward_navigation.rb +1 -0
- data/templates/default/layout/html/footer.erb +10 -0
- data/templates/default/layout/html/headers.erb +23 -0
- data/templates/default/layout/html/layout.erb +2 -2
- data/templates/default/layout/html/setup.rb +41 -2
- metadata +94 -47
- data/lib/kward/scratchpad_runner.rb +0 -56
- data/lib/kward/tab_driver.rb +0 -90
- /data/lib/kward/{editor_prompt.rb → cli/editor_prompt.rb} +0 -0
- /data/lib/kward/{editor_prompt_session.rb → cli/editor_prompt_session.rb} +0 -0
- /data/lib/kward/{diff_view_mode.rb → prompt_interface/editor/diff_view_mode.rb} +0 -0
- /data/lib/kward/{editor_mode.rb → prompt_interface/editor/editor_mode.rb} +0 -0
- /data/lib/kward/{session_diff.rb → sessions/diff.rb} +0 -0
- /data/lib/kward/{session_naming.rb → sessions/naming.rb} +0 -0
- /data/lib/kward/{session_trash.rb → sessions/trash.rb} +0 -0
- /data/lib/kward/{terminal_sequences.rb → terminal/sequences.rb} +0 -0
- /data/lib/kward/{transcript_export.rb → transcripts/transcript_export.rb} +0 -0
- /data/lib/kward/{path_guard.rb → workspace/path_guard.rb} +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
require_relative "
|
|
1
|
+
require_relative "editor_mode"
|
|
2
2
|
require_relative "../../text_boundary"
|
|
3
3
|
require_relative "buffer"
|
|
4
4
|
require_relative "file_marker"
|
|
@@ -180,6 +180,15 @@ module Kward
|
|
|
180
180
|
sync_vibe_state
|
|
181
181
|
end
|
|
182
182
|
|
|
183
|
+
def vibe_command_range
|
|
184
|
+
@vibe_state.command_range
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def vibe_command_range=(value)
|
|
188
|
+
@vibe_state.command_range = value
|
|
189
|
+
sync_vibe_state
|
|
190
|
+
end
|
|
191
|
+
|
|
183
192
|
def vibe_last_change
|
|
184
193
|
@vibe_state.last_change
|
|
185
194
|
end
|
|
@@ -207,15 +216,6 @@ module Kward
|
|
|
207
216
|
sync_vibe_state
|
|
208
217
|
end
|
|
209
218
|
|
|
210
|
-
def vibe_visual_block_insert
|
|
211
|
-
@vibe_state.visual_block_insert
|
|
212
|
-
end
|
|
213
|
-
|
|
214
|
-
def vibe_visual_block_insert=(value)
|
|
215
|
-
@vibe_state.visual_block_insert = value
|
|
216
|
-
sync_vibe_state
|
|
217
|
-
end
|
|
218
|
-
|
|
219
219
|
def vibe_marks
|
|
220
220
|
@vibe_state.marks
|
|
221
221
|
end
|
|
@@ -1289,10 +1289,10 @@ module Kward
|
|
|
1289
1289
|
@vibe_mode = @vibe_state.mode
|
|
1290
1290
|
@vibe_pending = @vibe_state.pending
|
|
1291
1291
|
@vibe_command = @vibe_state.command
|
|
1292
|
+
@vibe_command_range = @vibe_state.command_range
|
|
1292
1293
|
@vibe_last_change = @vibe_state.last_change
|
|
1293
1294
|
@vibe_last_find = @vibe_state.last_find
|
|
1294
1295
|
@vibe_last_visual_selection = @vibe_state.last_visual_selection
|
|
1295
|
-
@vibe_visual_block_insert = @vibe_state.visual_block_insert
|
|
1296
1296
|
@vibe_marks = @vibe_state.marks
|
|
1297
1297
|
@vibe_registers = @vibe_state.registers
|
|
1298
1298
|
@vibe_register_types = @vibe_state.register_types
|
|
@@ -53,6 +53,7 @@ module Kward
|
|
|
53
53
|
keywords: %w[begin if while for try let quote function macro module baremodule struct mutable abstract primitive type do end else elseif catch finally true false nothing]
|
|
54
54
|
},
|
|
55
55
|
makefile: {
|
|
56
|
+
extensions: %w[.mk],
|
|
56
57
|
filenames: %w[Makefile makefile GNUmakefile],
|
|
57
58
|
line_comment: "#",
|
|
58
59
|
keywords: %w[ifeq ifneq ifdef ifndef else endif include define endef export unexport override]
|
|
@@ -125,11 +126,17 @@ module Kward
|
|
|
125
126
|
def editor_highlight_line(line, line_index = nil)
|
|
126
127
|
return line.to_s unless @color_enabled
|
|
127
128
|
|
|
128
|
-
|
|
129
|
+
if editor_syntax_language == :markdown
|
|
130
|
+
editor_highlight_markdown(line, line_index)
|
|
131
|
+
else
|
|
132
|
+
editor_highlight_language_line(line, editor_syntax_language, line_index)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def editor_highlight_language_line(line, language, line_index = nil)
|
|
137
|
+
case language
|
|
129
138
|
when :ruby
|
|
130
139
|
editor_highlight_ruby(line, line_index)
|
|
131
|
-
when :markdown
|
|
132
|
-
editor_highlight_markdown(line)
|
|
133
140
|
when :json
|
|
134
141
|
editor_highlight_json(line)
|
|
135
142
|
when :yaml
|
|
@@ -143,7 +150,7 @@ module Kward
|
|
|
143
150
|
when :sql
|
|
144
151
|
editor_highlight_sql(line)
|
|
145
152
|
else
|
|
146
|
-
editor_highlight_generic(line,
|
|
153
|
+
editor_highlight_generic(line, language, line_index)
|
|
147
154
|
end
|
|
148
155
|
end
|
|
149
156
|
|
|
@@ -395,16 +402,71 @@ module Kward
|
|
|
395
402
|
nil
|
|
396
403
|
end
|
|
397
404
|
|
|
398
|
-
def editor_highlight_markdown(line)
|
|
405
|
+
def editor_highlight_markdown(line, line_index = nil)
|
|
399
406
|
text = line.to_s
|
|
407
|
+
fence_context = editor_markdown_fence_context(line_index)
|
|
408
|
+
return editor_highlight_markdown_fence(text) if fence_context == :marker
|
|
409
|
+
if fence_context.is_a?(Array)
|
|
410
|
+
language, nested_line_index = fence_context
|
|
411
|
+
return editor_highlight_language_line(text, language, nested_line_index) if language
|
|
412
|
+
|
|
413
|
+
return text
|
|
414
|
+
end
|
|
415
|
+
|
|
400
416
|
return editor_highlight_markdown_heading(text) if text.match?(/\A\s{0,3}[#]{1,6}\s/)
|
|
401
|
-
return editor_highlight_markdown_fence(text) if text.match?(/\A\s*```/)
|
|
402
417
|
return editor_highlight_markdown_blockquote(text) if text.match?(/\A\s*>/)
|
|
403
418
|
return editor_highlight_markdown_list(text) if text.match?(/\A\s*(?:[-*+]\s+|\d+\.\s+)/)
|
|
404
419
|
|
|
405
420
|
editor_highlight_markdown_inline(text)
|
|
406
421
|
end
|
|
407
422
|
|
|
423
|
+
def editor_markdown_fence_context(line_index)
|
|
424
|
+
return nil unless line_index && @editor_state
|
|
425
|
+
|
|
426
|
+
inside_fence = false
|
|
427
|
+
language = nil
|
|
428
|
+
opening_index = nil
|
|
429
|
+
@editor_state.lines.first(line_index.to_i + 1).each_with_index do |line, index|
|
|
430
|
+
match = line.to_s.match(/\A\s*```([^`]*)\s*\z/)
|
|
431
|
+
if match
|
|
432
|
+
if inside_fence
|
|
433
|
+
inside_fence = false
|
|
434
|
+
language = nil
|
|
435
|
+
opening_index = nil
|
|
436
|
+
else
|
|
437
|
+
inside_fence = true
|
|
438
|
+
language = editor_markdown_fence_language(match[1])
|
|
439
|
+
opening_index = index
|
|
440
|
+
end
|
|
441
|
+
return :marker if index == line_index
|
|
442
|
+
elsif inside_fence && index == line_index
|
|
443
|
+
return [language, index, opening_index]
|
|
444
|
+
end
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
nil
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
def editor_effective_syntax_language(line_index = nil)
|
|
451
|
+
language = editor_syntax_language
|
|
452
|
+
return language unless language == :markdown
|
|
453
|
+
|
|
454
|
+
context = editor_markdown_fence_context(line_index)
|
|
455
|
+
context.is_a?(Array) ? context.first || :markdown : :markdown
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
def editor_syntax_context_start(line_index = nil)
|
|
459
|
+
return nil unless editor_syntax_language == :markdown
|
|
460
|
+
|
|
461
|
+
context = editor_markdown_fence_context(line_index)
|
|
462
|
+
context.is_a?(Array) ? context[2] : nil
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
def editor_markdown_fence_language(info)
|
|
466
|
+
name = info.to_s.strip.split(/\s/, 2).first.to_s.downcase
|
|
467
|
+
ScratchpadLanguages::ALIASES[name]
|
|
468
|
+
end
|
|
469
|
+
|
|
408
470
|
def editor_highlight_markdown_heading(line)
|
|
409
471
|
line.sub(/\A(\s{0,3}[#]{1,6}\s+)(.*)\z/) do
|
|
410
472
|
"#{colored(Regexp.last_match(1), :cyan)}#{colored(Regexp.last_match(2), :bold)}"
|
|
@@ -5,7 +5,7 @@ module Kward
|
|
|
5
5
|
# Modal Vibe editor state for one editor buffer.
|
|
6
6
|
class VibeEditorState
|
|
7
7
|
attr_accessor :mode, :pending, :command, :last_change, :last_find
|
|
8
|
-
attr_accessor :
|
|
8
|
+
attr_accessor :command_range, :last_visual_selection
|
|
9
9
|
attr_accessor :marks, :registers, :register_types, :macros, :recording_macro, :last_macro
|
|
10
10
|
attr_accessor :kill_linewise, :previous_change_cursor, :jump_back_list, :jump_forward_list
|
|
11
11
|
|
|
@@ -13,10 +13,10 @@ module Kward
|
|
|
13
13
|
@mode = editor_mode == "vibe" ? "normal" : nil
|
|
14
14
|
@pending = ""
|
|
15
15
|
@command = ""
|
|
16
|
+
@command_range = nil
|
|
16
17
|
@last_change = nil
|
|
17
18
|
@last_find = nil
|
|
18
19
|
@last_visual_selection = nil
|
|
19
|
-
@visual_block_insert = nil
|
|
20
20
|
@marks = {}
|
|
21
21
|
@registers = {}
|
|
22
22
|
@register_types = {}
|
|
@@ -34,10 +34,10 @@ module Kward
|
|
|
34
34
|
state.mode = other.mode&.dup
|
|
35
35
|
state.pending = other.pending.dup
|
|
36
36
|
state.command = other.command.dup
|
|
37
|
+
state.command_range = other.command_range&.dup
|
|
37
38
|
state.last_change = other.last_change&.dup
|
|
38
39
|
state.last_find = other.last_find&.dup
|
|
39
40
|
state.last_visual_selection = other.last_visual_selection&.dup
|
|
40
|
-
state.visual_block_insert = other.visual_block_insert&.dup
|
|
41
41
|
state.marks = other.marks.transform_values(&:dup)
|
|
42
42
|
state.registers = other.registers.transform_values(&:dup)
|
|
43
43
|
state.register_types = other.register_types.transform_values(&:dup)
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
require_relative "../text_matcher"
|
|
2
|
+
|
|
1
3
|
# Namespace for the Kward CLI agent runtime.
|
|
2
4
|
module Kward
|
|
3
5
|
# File-mention completion overlay behavior.
|
|
@@ -86,32 +88,78 @@ module Kward
|
|
|
86
88
|
def file_overlay_matches
|
|
87
89
|
token = active_file_open || active_file_mention
|
|
88
90
|
return [] unless token
|
|
91
|
+
unless @file_mention_paths
|
|
92
|
+
start_file_mention_discovery_locked
|
|
93
|
+
return []
|
|
94
|
+
end
|
|
89
95
|
|
|
90
96
|
query = token[:query].downcase
|
|
97
|
+
entries = project_file_path_entries
|
|
98
|
+
cache = @file_overlay_match_cache
|
|
99
|
+
return cache[:matches] if cache && cache[:query] == query && cache[:entries].equal?(entries)
|
|
100
|
+
|
|
101
|
+
pattern = TextMatcher.subsequence_pattern(query)
|
|
91
102
|
matches = []
|
|
92
|
-
|
|
93
|
-
next unless
|
|
103
|
+
entries.each do |entry|
|
|
104
|
+
next unless TextMatcher.subsequence?(entry[:downcase], query, pattern)
|
|
94
105
|
|
|
95
106
|
matches << entry[:path]
|
|
96
107
|
break if matches.length >= FILE_MENTION_RESULT_LIMIT
|
|
97
108
|
end
|
|
109
|
+
@file_overlay_match_cache = { query: query, entries: entries, matches: matches }
|
|
98
110
|
matches
|
|
99
111
|
end
|
|
100
112
|
|
|
101
|
-
def
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
+
def start_file_mention_discovery_locked
|
|
114
|
+
root = prompt_workspace_root
|
|
115
|
+
active_thread = @file_mention_discovery_thread
|
|
116
|
+
return if active_thread&.alive? && @file_mention_discovery_root == root
|
|
117
|
+
|
|
118
|
+
token = Object.new
|
|
119
|
+
start = Queue.new
|
|
120
|
+
@file_mention_discovery_root = root
|
|
121
|
+
@file_mention_discovery_token = token
|
|
122
|
+
worker = Thread.new do
|
|
123
|
+
start.pop
|
|
124
|
+
paths = discover_file_mention_paths(root)
|
|
125
|
+
entries = build_file_path_entries(paths)
|
|
126
|
+
@mutex.synchronize do
|
|
127
|
+
next unless @file_mention_discovery_token.equal?(token)
|
|
128
|
+
|
|
129
|
+
@file_mention_discovery_thread = nil
|
|
130
|
+
@file_mention_discovery_root = nil
|
|
131
|
+
@file_mention_discovery_token = nil
|
|
132
|
+
next unless root == prompt_workspace_root
|
|
133
|
+
|
|
134
|
+
@file_mention_paths = paths
|
|
135
|
+
@file_mention_path_entries_paths = paths
|
|
136
|
+
@file_mention_path_entries = entries
|
|
137
|
+
@file_overlay_match_cache = nil
|
|
138
|
+
render_prompt_locked if @started && @asking && file_overlay_visible?
|
|
113
139
|
end
|
|
114
140
|
end
|
|
141
|
+
worker.report_on_exception = false
|
|
142
|
+
@file_mention_discovery_thread = worker
|
|
143
|
+
start << true
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def discover_file_mention_paths(root)
|
|
147
|
+
ProjectFiles.list(root: root)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def finish_file_mention_discovery_locked
|
|
151
|
+
root = prompt_workspace_root
|
|
152
|
+
@file_mention_discovery_thread = nil
|
|
153
|
+
@file_mention_discovery_root = nil
|
|
154
|
+
@file_mention_discovery_token = nil
|
|
155
|
+
@file_mention_paths = discover_file_mention_paths(root)
|
|
156
|
+
@file_mention_path_entries_paths = @file_mention_paths
|
|
157
|
+
@file_mention_path_entries = build_file_path_entries(@file_mention_paths)
|
|
158
|
+
@file_overlay_match_cache = nil
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def file_mention_discovery_loading?
|
|
162
|
+
@file_mention_discovery_thread&.alive? && @file_mention_discovery_root == prompt_workspace_root
|
|
115
163
|
end
|
|
116
164
|
|
|
117
165
|
def project_file_paths(include_ignored: false)
|
|
@@ -127,7 +175,11 @@ module Kward
|
|
|
127
175
|
return @file_mention_path_entries if @file_mention_path_entries_paths.equal?(paths) && @file_mention_path_entries
|
|
128
176
|
|
|
129
177
|
@file_mention_path_entries_paths = paths
|
|
130
|
-
@file_mention_path_entries = paths
|
|
178
|
+
@file_mention_path_entries = build_file_path_entries(paths)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def build_file_path_entries(paths)
|
|
182
|
+
paths.map { |path| { path: path, downcase: path.downcase } }
|
|
131
183
|
end
|
|
132
184
|
|
|
133
185
|
def discover_project_file_paths(include_ignored: false)
|
|
@@ -165,6 +217,7 @@ module Kward
|
|
|
165
217
|
def selected_file_overlay_path
|
|
166
218
|
return nil unless file_overlay_visible?
|
|
167
219
|
|
|
220
|
+
finish_file_mention_discovery_locked unless @file_mention_paths
|
|
168
221
|
matches = file_overlay_matches
|
|
169
222
|
return nil if matches.empty?
|
|
170
223
|
|
|
@@ -200,6 +253,9 @@ module Kward
|
|
|
200
253
|
return [] unless file_overlay_visible?
|
|
201
254
|
|
|
202
255
|
matches = file_overlay_matches
|
|
256
|
+
if file_mention_discovery_loading?
|
|
257
|
+
return overlay_card_rows("Files", [overlay_text_line("Loading project files…", :muted)], width)
|
|
258
|
+
end
|
|
203
259
|
if matches.empty?
|
|
204
260
|
return overlay_card_rows("Files", [overlay_text_line("No matching files", :muted)], width)
|
|
205
261
|
end
|
|
@@ -6,6 +6,73 @@ module Kward
|
|
|
6
6
|
module KeyHandler
|
|
7
7
|
private
|
|
8
8
|
|
|
9
|
+
def filter_terminal_handoff_input_locked(chunk)
|
|
10
|
+
@handoff_input_buffer << chunk.to_s.b
|
|
11
|
+
forwarded = +"".b
|
|
12
|
+
tab_action = nil
|
|
13
|
+
|
|
14
|
+
until @handoff_input_buffer.empty?
|
|
15
|
+
if @handoff_input_buffer.start_with?(TerminalKeys::CTRL_T)
|
|
16
|
+
action = handle_tab_key_binding(TerminalKeys::CTRL_T)
|
|
17
|
+
if action != false
|
|
18
|
+
remainder = @handoff_input_buffer.byteslice(TerminalKeys::CTRL_T.bytesize..).to_s
|
|
19
|
+
queue_pending_keys(remainder) unless remainder.empty?
|
|
20
|
+
@handoff_input_buffer.clear
|
|
21
|
+
tab_action = action
|
|
22
|
+
break
|
|
23
|
+
end
|
|
24
|
+
forwarded << @handoff_input_buffer.slice!(0)
|
|
25
|
+
next
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
if @handoff_input_buffer.start_with?("\e")
|
|
29
|
+
complete = TerminalKeys::TAB_ACTION_SEQUENCES.find { |sequence| @handoff_input_buffer.start_with?(sequence) }
|
|
30
|
+
partial = TerminalKeys::TAB_ACTION_SEQUENCES.any? { |sequence| sequence.start_with?(@handoff_input_buffer) }
|
|
31
|
+
break if partial && !complete
|
|
32
|
+
|
|
33
|
+
if complete
|
|
34
|
+
action = handle_tab_key_binding(complete)
|
|
35
|
+
if action != false
|
|
36
|
+
remainder = @handoff_input_buffer.byteslice(complete.bytesize..).to_s
|
|
37
|
+
queue_pending_keys(remainder) unless remainder.empty?
|
|
38
|
+
@handoff_input_buffer.clear
|
|
39
|
+
tab_action = action
|
|
40
|
+
break
|
|
41
|
+
end
|
|
42
|
+
forwarded << complete
|
|
43
|
+
@handoff_input_buffer = @handoff_input_buffer.byteslice(complete.bytesize..).to_s.b
|
|
44
|
+
next
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
if (sequence = parse_csi_u_key(@handoff_input_buffer)) && @handoff_input_buffer.start_with?(sequence[:sequence])
|
|
48
|
+
legacy_key = legacy_handoff_key(sequence)
|
|
49
|
+
unless legacy_key.nil?
|
|
50
|
+
forwarded << legacy_key
|
|
51
|
+
@handoff_input_buffer = @handoff_input_buffer.byteslice(sequence[:sequence].bytesize..).to_s.b
|
|
52
|
+
next
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
forwarded << @handoff_input_buffer.slice!(0)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
{ input: forwarded, tab_action: tab_action }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def legacy_handoff_key(sequence)
|
|
64
|
+
event = csi_u_key_event(sequence)
|
|
65
|
+
return event[:text] if event[:type] == :printable
|
|
66
|
+
return "\r" if event[:type] == :enter
|
|
67
|
+
return "\b" if event[:type] == :backspace
|
|
68
|
+
return "\t" if event[:type] == :tab
|
|
69
|
+
|
|
70
|
+
return unless event[:type] == :modified && ctrl_modifier?(event[:modifier])
|
|
71
|
+
return unless event[:code].between?(32, 126)
|
|
72
|
+
|
|
73
|
+
(event[:code] & 0x1f).chr
|
|
74
|
+
end
|
|
75
|
+
|
|
9
76
|
def read_key(nonblock: false)
|
|
10
77
|
pending = @pending_keys.shift unless @pending_keys.empty?
|
|
11
78
|
return pending if pending
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
require_relative "../terminal/text"
|
|
2
|
+
|
|
1
3
|
# Namespace for the Kward CLI agent runtime.
|
|
2
4
|
module Kward
|
|
3
5
|
# Shared overlay drawing helpers for prompt UI popups.
|
|
@@ -63,7 +65,7 @@ module Kward
|
|
|
63
65
|
|
|
64
66
|
def overlay_top_border(title, card_width)
|
|
65
67
|
title = visible_truncate(title.to_s, [card_width - 4, 1].max)
|
|
66
|
-
plain_length = ANSI.strip(title)
|
|
68
|
+
plain_length = TerminalText.width(ANSI.strip(title))
|
|
67
69
|
colored("╭", :primary_green) + " #{colored(title, :primary_green, :bold)} " + colored("─" * [card_width - plain_length - 4, 0].max, :primary_green) + colored("╮", :primary_green)
|
|
68
70
|
end
|
|
69
71
|
|
|
@@ -98,7 +100,7 @@ module Kward
|
|
|
98
100
|
end
|
|
99
101
|
|
|
100
102
|
def align_overlay_row(row, width)
|
|
101
|
-
plain_length = ANSI.strip(row)
|
|
103
|
+
plain_length = TerminalText.width(ANSI.strip(row))
|
|
102
104
|
padding = [width - plain_length, 0].max
|
|
103
105
|
left = overlay_left_padding(width, plain_length)
|
|
104
106
|
right = padding - left
|
|
@@ -152,14 +154,14 @@ module Kward
|
|
|
152
154
|
end
|
|
153
155
|
|
|
154
156
|
def visible_ljust(text, width)
|
|
155
|
-
text.to_s + (" " * [width - ANSI.strip(text.to_s)
|
|
157
|
+
text.to_s + (" " * [width - TerminalText.width(ANSI.strip(text.to_s)), 0].max)
|
|
156
158
|
end
|
|
157
159
|
|
|
158
160
|
def visible_truncate(text, width)
|
|
159
161
|
plain = ANSI.strip(text.to_s)
|
|
160
|
-
return text.to_s if plain
|
|
162
|
+
return text.to_s if TerminalText.width(plain) <= width
|
|
161
163
|
|
|
162
|
-
plain
|
|
164
|
+
TerminalText.truncate(plain, width)
|
|
163
165
|
end
|
|
164
166
|
|
|
165
167
|
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Namespace for the Kward CLI agent runtime.
|
|
2
|
+
module Kward
|
|
3
|
+
class PromptInterface
|
|
4
|
+
# Terminal implementations of the frontend-neutral plugin UI requests.
|
|
5
|
+
module PluginUIRequests
|
|
6
|
+
def request_plugin_ui(kind, payload, cancellation: nil)
|
|
7
|
+
cancellation&.raise_if_cancelled!
|
|
8
|
+
result = case kind.to_sym
|
|
9
|
+
when :question then ask_user_question(payload.fetch(:questions))
|
|
10
|
+
when :select then request_plugin_select(payload)
|
|
11
|
+
when :confirm then request_plugin_confirmation(payload)
|
|
12
|
+
when :input then request_plugin_input(payload)
|
|
13
|
+
else raise ArgumentError, "unsupported plugin UI request: #{kind}"
|
|
14
|
+
end
|
|
15
|
+
cancellation&.raise_if_cancelled!
|
|
16
|
+
result
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def request_plugin_select(payload)
|
|
22
|
+
options = payload.fetch(:options)
|
|
23
|
+
labels = options.map { |option| plugin_option_text(option) }
|
|
24
|
+
selected = with_plugin_ui_modal_state do
|
|
25
|
+
select(payload[:message] || "Choose an option", labels, title: payload.fetch(:title))
|
|
26
|
+
end
|
|
27
|
+
return nil if selected.nil?
|
|
28
|
+
|
|
29
|
+
option = options[labels.index(selected)]
|
|
30
|
+
option && option.fetch(:value)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def request_plugin_confirmation(payload)
|
|
34
|
+
yes = { label: "Yes", description: "Continue." }
|
|
35
|
+
no = { label: "No", description: "Cancel." }
|
|
36
|
+
answers = ask_user_question([
|
|
37
|
+
{
|
|
38
|
+
header: payload.fetch(:title),
|
|
39
|
+
question: payload.fetch(:message),
|
|
40
|
+
options: payload[:default] ? [yes, no] : [no, yes]
|
|
41
|
+
}
|
|
42
|
+
])
|
|
43
|
+
return false unless answers
|
|
44
|
+
|
|
45
|
+
answers.first[:answer].to_s.casecmp?("yes")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def request_plugin_input(payload)
|
|
49
|
+
with_plugin_ui_modal_state do
|
|
50
|
+
@mutex.synchronize do
|
|
51
|
+
self.composer_input = payload[:default].to_s
|
|
52
|
+
self.composer_cursor = composer_input.length
|
|
53
|
+
@composer.prefill_input = nil
|
|
54
|
+
@composer.clear_attachments
|
|
55
|
+
@pending_keys.clear
|
|
56
|
+
@busy = false
|
|
57
|
+
@asking = true
|
|
58
|
+
end
|
|
59
|
+
label = payload[:placeholder].to_s.empty? ? payload.fetch(:title) : "#{payload.fetch(:title)} · #{payload[:placeholder]}"
|
|
60
|
+
answer = ask("#{label}>")
|
|
61
|
+
answer.is_a?(String) ? answer : nil
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def with_plugin_ui_modal_state
|
|
66
|
+
saved_state = @mutex.synchronize do
|
|
67
|
+
state = begin_question_prompt_state
|
|
68
|
+
@question_prompt_active = true
|
|
69
|
+
state
|
|
70
|
+
end
|
|
71
|
+
yield
|
|
72
|
+
ensure
|
|
73
|
+
finish_question_prompt(saved_state) if saved_state
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def plugin_option_text(option)
|
|
77
|
+
description = option[:description].to_s
|
|
78
|
+
description.empty? ? option.fetch(:label) : "#{option.fetch(:label)} — #{description}"
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|