kward 0.80.1 → 0.81.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 +33 -0
- data/Gemfile.lock +2 -2
- data/README.md +1 -1
- data/doc/composer.md +5 -8
- data/doc/configuration.md +13 -3
- data/doc/permissions.md +1 -1
- data/doc/rpc.md +2 -1
- data/doc/sandboxing.md +4 -4
- data/doc/security.md +6 -9
- data/doc/shell.md +161 -196
- data/doc/skills.md +17 -5
- data/doc/tabs.md +1 -1
- data/doc/usage.md +6 -5
- data/lib/kward/ansi.rb +9 -1
- data/lib/kward/cli/commands.rb +7 -0
- data/lib/kward/cli/interactive_turn.rb +1 -1
- data/lib/kward/cli/plugins.rb +1 -1
- data/lib/kward/cli/project_skills.rb +99 -0
- data/lib/kward/cli/project_skills_commands.rb +87 -0
- data/lib/kward/cli/prompt_interface.rb +18 -4
- data/lib/kward/cli/rendering.rb +28 -2
- data/lib/kward/cli/runtime_helpers.rb +156 -19
- data/lib/kward/cli/sessions.rb +6 -4
- data/lib/kward/cli/settings.rb +1 -1
- data/lib/kward/cli/slash_commands.rb +6 -0
- data/lib/kward/cli/tabs.rb +3 -1
- data/lib/kward/cli.rb +19 -1
- data/lib/kward/config_files.rb +9 -4
- data/lib/kward/conversation.rb +8 -5
- data/lib/kward/ekwsh.rb +37 -16
- data/lib/kward/interactive_pty_runner.rb +88 -22
- data/lib/kward/prompt_interface/composer_controller.rb +13 -1
- data/lib/kward/prompt_interface/editor/controller.rb +4 -0
- data/lib/kward/prompt_interface/key_handler.rb +78 -43
- data/lib/kward/prompt_interface/overlay_renderer.rb +12 -0
- data/lib/kward/prompt_interface/screen.rb +5 -0
- data/lib/kward/prompt_interface.rb +93 -21
- data/lib/kward/prompts/commands.rb +2 -0
- data/lib/kward/prompts.rb +6 -6
- data/lib/kward/rpc/server.rb +1 -0
- data/lib/kward/session_store.rb +3 -2
- data/lib/kward/skills/registry.rb +52 -2
- data/lib/kward/skills/trust_coordinator.rb +45 -0
- data/lib/kward/skills/trust_store.rb +107 -0
- data/lib/kward/version.rb +1 -1
- metadata +5 -1
data/lib/kward/cli.rb
CHANGED
|
@@ -59,6 +59,8 @@ require_relative "cli/sysprompt"
|
|
|
59
59
|
require_relative "cli/stats"
|
|
60
60
|
require_relative "cli/openrouter_commands"
|
|
61
61
|
require_relative "cli/runtime_helpers"
|
|
62
|
+
require_relative "cli/project_skills"
|
|
63
|
+
require_relative "cli/project_skills_commands"
|
|
62
64
|
require_relative "cli/slash_commands"
|
|
63
65
|
require_relative "cli/memory_commands"
|
|
64
66
|
require_relative "cli/hook_commands"
|
|
@@ -94,6 +96,8 @@ module Kward
|
|
|
94
96
|
include CLI::Stats
|
|
95
97
|
include CLI::OpenRouterCommands
|
|
96
98
|
include CLI::RuntimeHelpers
|
|
99
|
+
include CLI::ProjectSkills
|
|
100
|
+
include CLI::ProjectSkillsCommands
|
|
97
101
|
include CLI::SlashCommands
|
|
98
102
|
include CLI::MemoryCommands
|
|
99
103
|
include CLI::HookCommands
|
|
@@ -232,6 +236,16 @@ module Kward
|
|
|
232
236
|
return
|
|
233
237
|
end
|
|
234
238
|
|
|
239
|
+
if @argv.first == "skills"
|
|
240
|
+
if help_option_arguments?(@argv[1..] || [])
|
|
241
|
+
print_command_help("skills")
|
|
242
|
+
return
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
handle_project_skills_cli_command(@argv[1..] || [])
|
|
246
|
+
return
|
|
247
|
+
end
|
|
248
|
+
|
|
235
249
|
if @argv.first == "edit"
|
|
236
250
|
if help_option_arguments?(@argv[1..] || [])
|
|
237
251
|
print_command_help("edit")
|
|
@@ -430,7 +444,8 @@ module Kward
|
|
|
430
444
|
end
|
|
431
445
|
|
|
432
446
|
def interactive_loop(agent: nil)
|
|
433
|
-
setup_interactive_prompt
|
|
447
|
+
setup_interactive_prompt(defer_warnings: true)
|
|
448
|
+
prepare_interactive_project_skills
|
|
434
449
|
session_store = interactive_session_store(agent)
|
|
435
450
|
@resumed_last_session = false
|
|
436
451
|
if session_store && @prompt.respond_to?(:update_tabs)
|
|
@@ -450,8 +465,10 @@ module Kward
|
|
|
450
465
|
|
|
451
466
|
print_visual_banner unless @resumed_last_session || @restored_tabs
|
|
452
467
|
render_resumed_last_session_transcript(agent.conversation) if @resumed_last_session
|
|
468
|
+
enable_interactive_warnings if respond_to?(:enable_interactive_warnings, true)
|
|
453
469
|
|
|
454
470
|
@pending_inputs = []
|
|
471
|
+
install_bang_completion_provider(agent)
|
|
455
472
|
|
|
456
473
|
loop do
|
|
457
474
|
if @pending_inputs.empty? && active_tab&.shell
|
|
@@ -543,6 +560,7 @@ module Kward
|
|
|
543
560
|
runtime_output("Goodbye.")
|
|
544
561
|
active_tab&.agent&.conversation || agent&.conversation
|
|
545
562
|
ensure
|
|
563
|
+
clear_bang_completion_provider
|
|
546
564
|
begin
|
|
547
565
|
stop_tabs if respond_to?(:stop_tabs, true)
|
|
548
566
|
@prompt.close if prompt_interface?
|
data/lib/kward/config_files.rb
CHANGED
|
@@ -917,8 +917,12 @@ module Kward
|
|
|
917
917
|
# Lists configured skills discovered under the config directory.
|
|
918
918
|
#
|
|
919
919
|
# @return [Array<Skill>] skill metadata available to the model
|
|
920
|
-
def skills(workspace_root: Dir.pwd, warning_sink: nil)
|
|
921
|
-
skills_registry(workspace_root: workspace_root, warning_sink: warning_sink).skills
|
|
920
|
+
def skills(workspace_root: Dir.pwd, warning_sink: nil, project_skill_paths: nil)
|
|
921
|
+
skills_registry(workspace_root: workspace_root, warning_sink: warning_sink, project_skill_paths: project_skill_paths).skills
|
|
922
|
+
end
|
|
923
|
+
|
|
924
|
+
def project_skill_candidates(workspace_root: Dir.pwd, warning_sink: nil)
|
|
925
|
+
skills_registry(workspace_root: workspace_root, warning_sink: warning_sink).project_skill_candidates
|
|
922
926
|
end
|
|
923
927
|
|
|
924
928
|
# @return [String] trusted user plugin directory
|
|
@@ -961,7 +965,7 @@ module Kward
|
|
|
961
965
|
skills_registry(workspace_root: workspace_root).read_skill_file(name, relative_path)
|
|
962
966
|
end
|
|
963
967
|
|
|
964
|
-
def skills_registry(workspace_root: Dir.pwd, warning_sink: nil)
|
|
968
|
+
def skills_registry(workspace_root: Dir.pwd, warning_sink: nil, project_skill_paths: nil)
|
|
965
969
|
Skills::Registry.new(
|
|
966
970
|
config_dir: config_dir,
|
|
967
971
|
workspace_root: workspace_root,
|
|
@@ -970,7 +974,8 @@ module Kward
|
|
|
970
974
|
max_file_bytes: MAX_SKILL_FILE_BYTES,
|
|
971
975
|
markdown_parser: ->(path) { Frontmatter.markdown_parts(path, lenient: true) },
|
|
972
976
|
inside_directory: method(:inside_directory?),
|
|
973
|
-
warning_sink: warning_sink || @warning_sink
|
|
977
|
+
warning_sink: warning_sink || @warning_sink,
|
|
978
|
+
project_skill_paths: project_skill_paths
|
|
974
979
|
)
|
|
975
980
|
end
|
|
976
981
|
|
data/lib/kward/conversation.rb
CHANGED
|
@@ -63,13 +63,16 @@ module Kward
|
|
|
63
63
|
attr_reader :tool_output_artifacts
|
|
64
64
|
# @return [ContextBudgetMeter] runtime context savings for this conversation
|
|
65
65
|
attr_reader :context_budget_meter
|
|
66
|
+
# @return [Array<String>, nil] explicitly permitted project skill paths
|
|
67
|
+
attr_reader :project_skill_paths
|
|
66
68
|
|
|
67
|
-
def initialize(system_message: DEFAULT_SYSTEM_MESSAGE, messages: [], read_paths: [], on_append: nil, on_compact: nil, on_tool_execution: nil, on_runtime_update: nil, workspace_root: Dir.pwd, compaction_system_message: DEFAULT_SYSTEM_MESSAGE, provider: nil, model: nil, reasoning_effort: nil, memory_context: nil, session_memories: [], last_memory_retrieval: nil, plugin_registry: nil, execution_profile_context: nil)
|
|
69
|
+
def initialize(system_message: DEFAULT_SYSTEM_MESSAGE, messages: [], read_paths: [], on_append: nil, on_compact: nil, on_tool_execution: nil, on_runtime_update: nil, workspace_root: Dir.pwd, compaction_system_message: DEFAULT_SYSTEM_MESSAGE, provider: nil, model: nil, reasoning_effort: nil, memory_context: nil, session_memories: [], last_memory_retrieval: nil, plugin_registry: nil, execution_profile_context: nil, project_skill_paths: nil)
|
|
68
70
|
@workspace_root = ConfigFiles.canonical_workspace_root(workspace_root)
|
|
69
71
|
@provider = provider
|
|
70
72
|
@model = model
|
|
71
73
|
@reasoning_effort = reasoning_effort
|
|
72
74
|
@plugin_registry = plugin_registry
|
|
75
|
+
@project_skill_paths = project_skill_paths
|
|
73
76
|
@execution_profile_context = execution_profile_context.to_s.empty? ? nil : execution_profile_context.to_s.freeze
|
|
74
77
|
@messages = []
|
|
75
78
|
restored_system_message, transcript_messages = split_system_message(messages)
|
|
@@ -78,13 +81,13 @@ module Kward
|
|
|
78
81
|
system_message = restored_system_message
|
|
79
82
|
else
|
|
80
83
|
@last_plugin_prompt_context = plugin_prompt_context
|
|
81
|
-
system_message = Prompts.system_message(workspace_root: @workspace_root, model: @model, reasoning_effort: @reasoning_effort, now: prompt_time, memory_context: memory_context, plugin_context: @last_plugin_prompt_context, execution_profile_context: @execution_profile_context)
|
|
84
|
+
system_message = Prompts.system_message(workspace_root: @workspace_root, model: @model, reasoning_effort: @reasoning_effort, now: prompt_time, memory_context: memory_context, plugin_context: @last_plugin_prompt_context, execution_profile_context: @execution_profile_context, project_skill_paths: @project_skill_paths)
|
|
82
85
|
end
|
|
83
86
|
end
|
|
84
87
|
@system_message = system_message
|
|
85
88
|
@system_message_enabled = !@system_message.nil?
|
|
86
89
|
if compaction_system_message.equal?(DEFAULT_SYSTEM_MESSAGE)
|
|
87
|
-
compaction_system_message = @system_message_enabled ? Prompts.system_message(workspace_root: @workspace_root, include_workspace_personality: false, model: @model, reasoning_effort: @reasoning_effort, now: prompt_time, execution_profile_context: @execution_profile_context) : nil
|
|
90
|
+
compaction_system_message = @system_message_enabled ? Prompts.system_message(workspace_root: @workspace_root, include_workspace_personality: false, model: @model, reasoning_effort: @reasoning_effort, now: prompt_time, execution_profile_context: @execution_profile_context, project_skill_paths: @project_skill_paths) : nil
|
|
88
91
|
end
|
|
89
92
|
@compaction_system_message = compaction_system_message
|
|
90
93
|
@workspace_agents_mtime = workspace_agents_mtime
|
|
@@ -188,10 +191,10 @@ module Kward
|
|
|
188
191
|
return nil unless @system_message_enabled
|
|
189
192
|
|
|
190
193
|
@last_plugin_prompt_context = plugin_prompt_context
|
|
191
|
-
replacement = Prompts.system_message(workspace_root: @workspace_root, model: @model, reasoning_effort: @reasoning_effort, now: prompt_time, memory_context: @memory_context, plugin_context: @last_plugin_prompt_context, execution_profile_context: @execution_profile_context)
|
|
194
|
+
replacement = Prompts.system_message(workspace_root: @workspace_root, model: @model, reasoning_effort: @reasoning_effort, now: prompt_time, memory_context: @memory_context, plugin_context: @last_plugin_prompt_context, execution_profile_context: @execution_profile_context, project_skill_paths: @project_skill_paths)
|
|
192
195
|
@system_message = replacement
|
|
193
196
|
@on_system_message_change&.call(replacement)
|
|
194
|
-
@compaction_system_message = Prompts.system_message(workspace_root: @workspace_root, include_workspace_personality: false, model: @model, reasoning_effort: @reasoning_effort, now: prompt_time, execution_profile_context: @execution_profile_context)
|
|
197
|
+
@compaction_system_message = Prompts.system_message(workspace_root: @workspace_root, include_workspace_personality: false, model: @model, reasoning_effort: @reasoning_effort, now: prompt_time, execution_profile_context: @execution_profile_context, project_skill_paths: @project_skill_paths)
|
|
195
198
|
@workspace_agents_mtime = workspace_agents_mtime
|
|
196
199
|
@system_prompt_sources_fingerprint = ConfigFiles.system_prompt_sources_fingerprint
|
|
197
200
|
replacement
|
data/lib/kward/ekwsh.rb
CHANGED
|
@@ -13,7 +13,7 @@ module Kward
|
|
|
13
13
|
class Ekwsh
|
|
14
14
|
Result = Struct.new(:output, :exit_status, :exit_shell, :clear, :open_editor_path, :interactive_command, :streamed, keyword_init: true)
|
|
15
15
|
Completion = Struct.new(:range, :replacement, :candidates, keyword_init: true)
|
|
16
|
-
BUILTINS = %w[alias cd pwd export unset unalias clear exit logout pty].freeze
|
|
16
|
+
BUILTINS = %w[alias capture cd pwd export unset unalias clear exit logout pty].freeze
|
|
17
17
|
DEFAULT_SHELL = "/bin/sh"
|
|
18
18
|
DEFAULT_TIMEOUT_SECONDS = 300
|
|
19
19
|
DEFAULT_MAX_OUTPUT_BYTES = 1_048_576
|
|
@@ -72,6 +72,23 @@ module Kward
|
|
|
72
72
|
Completion.new(range: token[:range], replacement: replacement, candidates: candidates)
|
|
73
73
|
end
|
|
74
74
|
|
|
75
|
+
def expand_alias(command, interactive: false)
|
|
76
|
+
words = shell_words(command)
|
|
77
|
+
return command if words.empty? || BUILTINS.include?(words.first)
|
|
78
|
+
return command unless @aliases[words.first]
|
|
79
|
+
|
|
80
|
+
rest = command.sub(/\A\s*#{Regexp.escape(words.first)}\b\s*/, "")
|
|
81
|
+
expanded = [@aliases.fetch(words.first), rest].reject(&:empty?).join(" ")
|
|
82
|
+
interactive ? expanded.sub(/\A\s*(?:capture|pty)(?:\s+|\z)/, "") : expanded
|
|
83
|
+
rescue ArgumentError
|
|
84
|
+
command
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def editor_command_result(command, display_command: command)
|
|
88
|
+
expanded_command = expand_alias(command, interactive: true)
|
|
89
|
+
kward_command_result(expanded_command, display_command: display_command)
|
|
90
|
+
end
|
|
91
|
+
|
|
75
92
|
private
|
|
76
93
|
|
|
77
94
|
def configure_rbenv_environment
|
|
@@ -276,7 +293,7 @@ module Kward
|
|
|
276
293
|
Result.new(output: "#{command_echo(display_command)}ekwsh: #{e.message}\n", exit_status: 2)
|
|
277
294
|
end
|
|
278
295
|
|
|
279
|
-
def builtin_result(command, display_command: command)
|
|
296
|
+
def builtin_result(command, display_command: command, cancellation: nil, &block)
|
|
280
297
|
words = shell_words(command)
|
|
281
298
|
return nil if words.empty?
|
|
282
299
|
assignment_result = persist_assignments(display_command, words)
|
|
@@ -297,6 +314,8 @@ module Kward
|
|
|
297
314
|
unset_variables(display_command, words)
|
|
298
315
|
when "clear"
|
|
299
316
|
Result.new(output: "", exit_status: 0, clear: true)
|
|
317
|
+
when "capture"
|
|
318
|
+
captured_command_result(command, display_command: display_command, cancellation: cancellation, &block)
|
|
300
319
|
when "pty"
|
|
301
320
|
interactive_pty_result(command, display_command: display_command)
|
|
302
321
|
else
|
|
@@ -306,13 +325,26 @@ module Kward
|
|
|
306
325
|
Result.new(output: "#{command_echo(display_command)}ekwsh: #{e.message}\n", exit_status: 2)
|
|
307
326
|
end
|
|
308
327
|
|
|
328
|
+
def captured_command_result(command, display_command:, cancellation: nil, &block)
|
|
329
|
+
captured_command = command.sub(/\A\s*capture(?:\s+|\z)/, "")
|
|
330
|
+
if captured_command.empty?
|
|
331
|
+
return Result.new(output: "#{command_echo(display_command)}Usage: capture <command>\n", exit_status: 2)
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
execute(captured_command, display_command: display_command, cancellation: cancellation, &block)
|
|
335
|
+
end
|
|
336
|
+
|
|
309
337
|
def interactive_pty_result(command, display_command: command)
|
|
310
338
|
interactive_command = command.sub(/\A\s*pty(?:\s+|\z)/, "")
|
|
311
339
|
if interactive_command.empty?
|
|
312
340
|
return Result.new(output: "#{command_echo(display_command)}Usage: pty <command>\n", exit_status: 2)
|
|
313
341
|
end
|
|
314
342
|
|
|
315
|
-
|
|
343
|
+
interactive_command_result(interactive_command, display_command: display_command)
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def interactive_command_result(command, display_command: command)
|
|
347
|
+
Result.new(output: command_echo(display_command), exit_status: 0, interactive_command: command)
|
|
316
348
|
end
|
|
317
349
|
|
|
318
350
|
def shell_words(command)
|
|
@@ -361,29 +393,18 @@ module Kward
|
|
|
361
393
|
name.to_s.match?(/\A[A-Za-z_][A-Za-z0-9_-]*\z/) && !BUILTINS.include?(name.to_s)
|
|
362
394
|
end
|
|
363
395
|
|
|
364
|
-
def expand_alias(command)
|
|
365
|
-
words = shell_words(command)
|
|
366
|
-
return command if words.empty? || BUILTINS.include?(words.first)
|
|
367
|
-
return command unless @aliases[words.first]
|
|
368
|
-
|
|
369
|
-
rest = command.sub(/\A\s*#{Regexp.escape(words.first)}\b\s*/, "")
|
|
370
|
-
[@aliases.fetch(words.first), rest].reject(&:empty?).join(" ")
|
|
371
|
-
rescue ArgumentError
|
|
372
|
-
command
|
|
373
|
-
end
|
|
374
|
-
|
|
375
396
|
def run_expanded_command(command, cancellation: nil, &block)
|
|
376
397
|
expanded_command = expand_alias(command)
|
|
377
398
|
exit_result = exit_result(expanded_command, display_command: command)
|
|
378
399
|
return exit_result if exit_result
|
|
379
400
|
|
|
380
|
-
builtin_result = builtin_result(expanded_command, display_command: command)
|
|
401
|
+
builtin_result = builtin_result(expanded_command, display_command: command, cancellation: cancellation, &block)
|
|
381
402
|
return builtin_result if builtin_result
|
|
382
403
|
|
|
383
404
|
kward_result = kward_command_result(expanded_command, display_command: command)
|
|
384
405
|
return kward_result if kward_result
|
|
385
406
|
|
|
386
|
-
|
|
407
|
+
interactive_command_result(expanded_command, display_command: command)
|
|
387
408
|
end
|
|
388
409
|
|
|
389
410
|
def kward_command_result(command, display_command: command)
|
|
@@ -9,7 +9,7 @@ module Kward
|
|
|
9
9
|
# This is intentionally low level: UI orchestration decides when terminal
|
|
10
10
|
# ownership is handed to the child process and how the result is presented.
|
|
11
11
|
class InteractivePtyRunner
|
|
12
|
-
Result = Struct.new(:exit_status, keyword_init: true)
|
|
12
|
+
Result = Struct.new(:exit_status, :input_forwarded, keyword_init: true)
|
|
13
13
|
|
|
14
14
|
READ_SIZE = 4096
|
|
15
15
|
|
|
@@ -18,34 +18,27 @@ module Kward
|
|
|
18
18
|
@window_size = nil
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
-
def run(*command, env: {}, cwd: Dir.pwd, input: $stdin, output: $stdout)
|
|
21
|
+
def run(*command, env: {}, cwd: Dir.pwd, input: $stdin, output: $stdout, &block)
|
|
22
22
|
pid = nil
|
|
23
23
|
status = nil
|
|
24
|
+
writer = nil
|
|
25
|
+
input_forwarded = false
|
|
24
26
|
|
|
25
|
-
PTY.spawn(env.to_h, *command, chdir: cwd.to_s) do |reader,
|
|
27
|
+
PTY.spawn(env.to_h, *command, chdir: cwd.to_s) do |reader, child_writer, child_pid|
|
|
26
28
|
pid = child_pid
|
|
29
|
+
writer = child_writer
|
|
27
30
|
update_window_size(reader, pid)
|
|
28
31
|
with_raw_input(input) do
|
|
29
32
|
drain_initial_input(input, writer)
|
|
30
|
-
|
|
31
|
-
update_window_size(reader, pid)
|
|
32
|
-
readable = IO.select([reader, input], nil, nil, 0.02)&.first || []
|
|
33
|
-
forward_pty_output(reader, output) if readable.include?(reader)
|
|
34
|
-
forward_input(input, writer) if readable.include?(input)
|
|
35
|
-
if (finished_status = finished_status(pid))
|
|
36
|
-
status = finished_status
|
|
37
|
-
break
|
|
38
|
-
end
|
|
39
|
-
rescue Errno::EIO, IOError
|
|
40
|
-
break
|
|
41
|
-
end
|
|
33
|
+
status, input_forwarded = forward_io(reader, writer, pid, input, output, &block)
|
|
42
34
|
end
|
|
43
35
|
status ||= wait_for_status(pid)
|
|
44
|
-
ensure
|
|
45
|
-
writer&.close unless writer&.closed?
|
|
46
36
|
end
|
|
47
37
|
|
|
48
|
-
Result.new(exit_status: exit_status(status))
|
|
38
|
+
Result.new(exit_status: exit_status(status), input_forwarded: input_forwarded)
|
|
39
|
+
ensure
|
|
40
|
+
writer&.close unless writer&.closed?
|
|
41
|
+
terminate_and_reap(pid) if pid && status.nil?
|
|
49
42
|
end
|
|
50
43
|
|
|
51
44
|
private
|
|
@@ -53,8 +46,14 @@ module Kward
|
|
|
53
46
|
def with_raw_input(input)
|
|
54
47
|
return yield unless input.respond_to?(:raw)
|
|
55
48
|
|
|
56
|
-
|
|
49
|
+
yielded = false
|
|
50
|
+
input.raw do
|
|
51
|
+
yielded = true
|
|
52
|
+
yield
|
|
53
|
+
end
|
|
57
54
|
rescue Errno::ENOTTY
|
|
55
|
+
raise if yielded
|
|
56
|
+
|
|
58
57
|
yield
|
|
59
58
|
end
|
|
60
59
|
|
|
@@ -70,22 +69,67 @@ module Kward
|
|
|
70
69
|
nil
|
|
71
70
|
end
|
|
72
71
|
|
|
72
|
+
def forward_io(reader, writer, pid, input, output, &block)
|
|
73
|
+
input_open = true
|
|
74
|
+
input_forwarded = false
|
|
75
|
+
loop do
|
|
76
|
+
update_window_size(reader, pid)
|
|
77
|
+
readers = [reader]
|
|
78
|
+
readers << input if input_open
|
|
79
|
+
readable = IO.select(readers, nil, nil, 0.02)&.first || []
|
|
80
|
+
forward_pty_output(reader, output, &block) if readable.include?(reader)
|
|
81
|
+
if input_open && readable.include?(input)
|
|
82
|
+
input_open, forwarded = forward_input(input, writer)
|
|
83
|
+
input_forwarded ||= forwarded
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
status = finished_status(pid)
|
|
87
|
+
next unless status
|
|
88
|
+
|
|
89
|
+
return [finish_stopped_process(pid), input_forwarded] if status.stopped?
|
|
90
|
+
|
|
91
|
+
drain_pty_output(reader, output, &block)
|
|
92
|
+
return [status, input_forwarded]
|
|
93
|
+
rescue Errno::EIO
|
|
94
|
+
return [wait_for_status(pid), input_forwarded]
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
73
98
|
def forward_pty_output(reader, output)
|
|
74
99
|
chunk = reader.read_nonblock(READ_SIZE, exception: false)
|
|
75
100
|
return if chunk.nil? || chunk == :wait_readable
|
|
76
101
|
|
|
77
102
|
output.write(chunk)
|
|
78
103
|
output.flush if output.respond_to?(:flush)
|
|
104
|
+
yield chunk if block_given?
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def drain_pty_output(reader, output)
|
|
108
|
+
loop do
|
|
109
|
+
readable, = IO.select([reader], nil, nil, 0.02)
|
|
110
|
+
break unless readable
|
|
111
|
+
|
|
112
|
+
chunk = reader.read_nonblock(READ_SIZE, exception: false)
|
|
113
|
+
break if chunk.nil? || chunk == :wait_readable
|
|
114
|
+
|
|
115
|
+
output.write(chunk)
|
|
116
|
+
yield chunk if block_given?
|
|
117
|
+
end
|
|
118
|
+
output.flush if output.respond_to?(:flush)
|
|
119
|
+
rescue Errno::EIO
|
|
120
|
+
nil
|
|
79
121
|
end
|
|
80
122
|
|
|
81
123
|
def forward_input(input, writer)
|
|
82
124
|
chunk = input.read_nonblock(READ_SIZE, exception: false)
|
|
83
|
-
return if chunk.nil?
|
|
125
|
+
return [false, false] if chunk.nil?
|
|
126
|
+
return [true, false] if chunk == :wait_readable
|
|
84
127
|
|
|
85
128
|
writer.write(chunk)
|
|
86
129
|
writer.flush
|
|
130
|
+
[true, true]
|
|
87
131
|
rescue Errno::EIO, Errno::EPIPE, IOError
|
|
88
|
-
|
|
132
|
+
[false, false]
|
|
89
133
|
end
|
|
90
134
|
|
|
91
135
|
def update_window_size(reader, pid)
|
|
@@ -111,12 +155,18 @@ module Kward
|
|
|
111
155
|
def finished_status(pid)
|
|
112
156
|
return unless pid
|
|
113
157
|
|
|
114
|
-
finished_pid, status = Process.wait2(pid, Process::WNOHANG)
|
|
158
|
+
finished_pid, status = Process.wait2(pid, Process::WNOHANG | Process::WUNTRACED)
|
|
115
159
|
status if finished_pid
|
|
116
160
|
rescue Errno::ECHILD
|
|
117
161
|
nil
|
|
118
162
|
end
|
|
119
163
|
|
|
164
|
+
def finish_stopped_process(pid)
|
|
165
|
+
signal_process("CONT", -pid) || signal_process("CONT", pid)
|
|
166
|
+
terminate_process_group(pid)
|
|
167
|
+
wait_for_status(pid)
|
|
168
|
+
end
|
|
169
|
+
|
|
120
170
|
def wait_for_status(pid)
|
|
121
171
|
return unless pid
|
|
122
172
|
|
|
@@ -134,6 +184,22 @@ module Kward
|
|
|
134
184
|
1
|
|
135
185
|
end
|
|
136
186
|
|
|
187
|
+
def terminate_and_reap(pid)
|
|
188
|
+
terminate_process_group(pid)
|
|
189
|
+
wait_for_status(pid)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def terminate_process_group(pid)
|
|
193
|
+
signal_process("TERM", -pid) || signal_process("TERM", pid)
|
|
194
|
+
deadline = Time.now + 0.2
|
|
195
|
+
while Time.now < deadline
|
|
196
|
+
return unless process_running?(pid)
|
|
197
|
+
|
|
198
|
+
sleep 0.02
|
|
199
|
+
end
|
|
200
|
+
signal_process("KILL", -pid) || signal_process("KILL", pid)
|
|
201
|
+
end
|
|
202
|
+
|
|
137
203
|
def process_running?(pid)
|
|
138
204
|
Process.kill(0, pid)
|
|
139
205
|
true
|
|
@@ -256,11 +256,23 @@ module Kward
|
|
|
256
256
|
def submit_input
|
|
257
257
|
value = submitted_input
|
|
258
258
|
add_history(composer_input)
|
|
259
|
-
|
|
259
|
+
if !@busy && value.strip.empty?
|
|
260
|
+
@output_io.flush
|
|
261
|
+
return value
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
clear_submitted_input_locked
|
|
260
265
|
@output_io.flush
|
|
261
266
|
value
|
|
262
267
|
end
|
|
263
268
|
|
|
269
|
+
def clear_submitted_input_locked
|
|
270
|
+
self.composer_input = ""
|
|
271
|
+
self.composer_cursor = 0
|
|
272
|
+
@composer.clear_attachments
|
|
273
|
+
reset_history_search
|
|
274
|
+
end
|
|
275
|
+
|
|
264
276
|
def clear_finished_input_locked(reset_history: false)
|
|
265
277
|
if @busy
|
|
266
278
|
clear_prompt_for_output_locked
|
|
@@ -827,6 +827,10 @@ module Kward
|
|
|
827
827
|
modifier = sequence[:modifier]
|
|
828
828
|
queue_pending_keys(sequence[:remaining]) if sequence[:remaining] && !sequence[:remaining].empty?
|
|
829
829
|
|
|
830
|
+
if ctrl_modifier?(modifier) && ctrl_code(code) == 113
|
|
831
|
+
return close_editor
|
|
832
|
+
end
|
|
833
|
+
|
|
830
834
|
if ctrl_modifier?(modifier) && ctrl_code(code) == 102
|
|
831
835
|
return editor_search_active? ? editor_search_append(key) : editor_search_begin
|
|
832
836
|
end
|
|
@@ -36,6 +36,9 @@ module Kward
|
|
|
36
36
|
return true if handle_mouse_reporting_key(key)
|
|
37
37
|
return if handle_bracketed_paste_key(key)
|
|
38
38
|
|
|
39
|
+
completion_overlay_result = handle_completion_overlay_key(key)
|
|
40
|
+
return completion_overlay_result unless completion_overlay_result == false
|
|
41
|
+
|
|
39
42
|
csi_result = handle_csi_u_key(key)
|
|
40
43
|
return csi_result unless csi_result == false
|
|
41
44
|
return if handle_shift_enter_key(key)
|
|
@@ -44,9 +47,6 @@ module Kward
|
|
|
44
47
|
completion_result = handle_completion_provider_key(key)
|
|
45
48
|
return completion_result unless completion_result == false
|
|
46
49
|
|
|
47
|
-
reasoning_result = handle_reasoning_key_binding(key)
|
|
48
|
-
return reasoning_result unless reasoning_result == false
|
|
49
|
-
|
|
50
50
|
tab_result = handle_tab_key_binding(key)
|
|
51
51
|
return tab_result unless tab_result == false
|
|
52
52
|
|
|
@@ -81,7 +81,7 @@ module Kward
|
|
|
81
81
|
when :ctrl_y
|
|
82
82
|
yank_kill_buffer
|
|
83
83
|
when :ctrl_l
|
|
84
|
-
|
|
84
|
+
request_transcript_redraw
|
|
85
85
|
when :ctrl_r
|
|
86
86
|
start_history_search
|
|
87
87
|
when :left
|
|
@@ -212,9 +212,6 @@ module Kward
|
|
|
212
212
|
return true if sequence == "\e" && (dismiss_file_overlay || dismiss_slash_overlay)
|
|
213
213
|
return true if handle_shift_enter_key(sequence)
|
|
214
214
|
|
|
215
|
-
reasoning_result = handle_reasoning_key_binding(sequence)
|
|
216
|
-
return reasoning_result unless reasoning_result == false
|
|
217
|
-
|
|
218
215
|
tab_result = handle_tab_key_binding(sequence)
|
|
219
216
|
return tab_result unless tab_result == false
|
|
220
217
|
|
|
@@ -323,10 +320,10 @@ module Kward
|
|
|
323
320
|
if ctrl_modifier?(modifier)
|
|
324
321
|
shift_modifier?(modifier) ? { tab_action: :previous } : { tab_action: :next }
|
|
325
322
|
elsif shift_modifier?(modifier)
|
|
326
|
-
|
|
323
|
+
handle_tab_completion_key
|
|
327
324
|
else
|
|
328
325
|
completion_result = handle_completion_provider_key("\t")
|
|
329
|
-
completion_result == false ?
|
|
326
|
+
completion_result == false ? handle_tab_completion_key : completion_result
|
|
330
327
|
end
|
|
331
328
|
when 13
|
|
332
329
|
if modifier == 2
|
|
@@ -447,7 +444,7 @@ module Kward
|
|
|
447
444
|
when 107
|
|
448
445
|
kill_line_after_cursor
|
|
449
446
|
when 108
|
|
450
|
-
|
|
447
|
+
request_transcript_redraw
|
|
451
448
|
when 114
|
|
452
449
|
start_history_search
|
|
453
450
|
when 117
|
|
@@ -580,61 +577,99 @@ module Kward
|
|
|
580
577
|
|
|
581
578
|
CTRL_TAB_SEQUENCES = TerminalKeys::CTRL_TAB
|
|
582
579
|
CTRL_SHIFT_TAB_SEQUENCES = TerminalKeys::CTRL_SHIFT_TAB
|
|
583
|
-
|
|
580
|
+
|
|
581
|
+
def handle_completion_overlay_key(key)
|
|
582
|
+
return false unless @completion_overlay
|
|
583
|
+
|
|
584
|
+
return handle_completion_provider_key("\t") if completion_tab_key?(key)
|
|
585
|
+
|
|
586
|
+
dismiss_completion_overlay
|
|
587
|
+
false
|
|
588
|
+
end
|
|
589
|
+
|
|
590
|
+
def completion_tab_key?(key)
|
|
591
|
+
return true if key == "\t"
|
|
592
|
+
|
|
593
|
+
sequence = parse_csi_u_key(key)
|
|
594
|
+
return false unless sequence && sequence[:code] == 9
|
|
595
|
+
|
|
596
|
+
completion = !ctrl_modifier?(sequence[:modifier]) && !shift_modifier?(sequence[:modifier])
|
|
597
|
+
queue_pending_keys(sequence[:remaining]) if completion && sequence[:remaining] && !sequence[:remaining].empty?
|
|
598
|
+
completion
|
|
599
|
+
end
|
|
600
|
+
|
|
601
|
+
def dismiss_completion_overlay
|
|
602
|
+
@completion_overlay = nil
|
|
603
|
+
@completion_cycle = nil
|
|
604
|
+
end
|
|
584
605
|
|
|
585
606
|
def handle_completion_provider_key(key)
|
|
586
607
|
return false unless key == "\t" && @completion_provider
|
|
587
608
|
|
|
609
|
+
if (cycle = matching_completion_cycle)
|
|
610
|
+
cycle_completion(cycle)
|
|
611
|
+
return true
|
|
612
|
+
end
|
|
613
|
+
|
|
588
614
|
result = @completion_provider.call(composer_input, composer_cursor)
|
|
615
|
+
return false if result == false
|
|
589
616
|
return true unless result
|
|
590
617
|
|
|
591
618
|
apply_completion_result(result)
|
|
592
619
|
true
|
|
593
620
|
end
|
|
594
621
|
|
|
622
|
+
def matching_completion_cycle
|
|
623
|
+
cycle = @completion_cycle
|
|
624
|
+
return nil unless cycle
|
|
625
|
+
return cycle if cycle[:input] == composer_input && cycle[:cursor] == composer_cursor
|
|
626
|
+
|
|
627
|
+
@completion_cycle = nil
|
|
628
|
+
@completion_overlay = nil
|
|
629
|
+
nil
|
|
630
|
+
end
|
|
631
|
+
|
|
632
|
+
def cycle_completion(cycle)
|
|
633
|
+
cycle[:index] = (cycle[:index] + 1) % cycle[:candidates].length
|
|
634
|
+
apply_completion_cycle_candidate(cycle)
|
|
635
|
+
end
|
|
636
|
+
|
|
595
637
|
def apply_completion_result(result)
|
|
596
638
|
range = result[:range] || result["range"] || result.range
|
|
597
639
|
replacement = result[:replacement] || result["replacement"] || result.replacement
|
|
598
|
-
candidates = result[:candidates] || result["candidates"] || result.candidates
|
|
640
|
+
candidates = (result[:candidates] || result["candidates"] || result.candidates).to_a
|
|
599
641
|
original = composer_input
|
|
600
642
|
before = original[0...range.begin].to_s
|
|
601
643
|
after = original[range.end..].to_s
|
|
644
|
+
|
|
645
|
+
if candidates.length > 1
|
|
646
|
+
@completion_cycle = {
|
|
647
|
+
candidates: candidates.map(&:to_s),
|
|
648
|
+
index: 0,
|
|
649
|
+
before: before,
|
|
650
|
+
after: after
|
|
651
|
+
}
|
|
652
|
+
apply_completion_cycle_candidate(@completion_cycle)
|
|
653
|
+
show_completion_overlay(candidates)
|
|
654
|
+
return
|
|
655
|
+
end
|
|
656
|
+
|
|
657
|
+
@completion_cycle = nil
|
|
602
658
|
self.composer_input = "#{before}#{replacement}#{after}"
|
|
603
659
|
self.composer_cursor = before.length + replacement.to_s.length
|
|
604
|
-
show_completion_candidates(candidates, replacement) if candidates.to_a.length > 1 && replacement.to_s == original[range]
|
|
605
660
|
end
|
|
606
661
|
|
|
607
|
-
def
|
|
608
|
-
|
|
609
|
-
text = ["completions:", *lines.map { |candidate| " #{candidate}" }].join("\n")
|
|
610
|
-
write_completion_transcript_locked(text)
|
|
662
|
+
def show_completion_overlay(candidates)
|
|
663
|
+
@completion_overlay = { candidates: candidates.map(&:to_s), index: 0 }
|
|
611
664
|
end
|
|
612
665
|
|
|
613
|
-
def
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
def handle_reasoning_key_binding(key)
|
|
622
|
-
return false if @busy || @select_state || @question_state
|
|
623
|
-
return false if file_overlay_visible? || slash_overlay_visible?
|
|
624
|
-
return false if @slash_overlay_dismissed_input && @slash_overlay_dismissed_input == composer_input
|
|
625
|
-
mention_token = active_file_mention_token
|
|
626
|
-
open_token = active_file_open_token
|
|
627
|
-
return false if mention_token && @file_overlay_dismissed_token == mention_token
|
|
628
|
-
return false if open_token && @file_open_dismissed_token == open_token
|
|
629
|
-
|
|
630
|
-
case key
|
|
631
|
-
when "\t"
|
|
632
|
-
{ reasoning_action: :next }
|
|
633
|
-
when *SHIFT_TAB_SEQUENCES
|
|
634
|
-
{ reasoning_action: :previous }
|
|
635
|
-
else
|
|
636
|
-
false
|
|
637
|
-
end
|
|
666
|
+
def apply_completion_cycle_candidate(cycle)
|
|
667
|
+
replacement = cycle[:candidates][cycle[:index]]
|
|
668
|
+
self.composer_input = "#{cycle[:before]}#{replacement}#{cycle[:after]}"
|
|
669
|
+
self.composer_cursor = cycle[:before].length + replacement.length
|
|
670
|
+
@completion_overlay[:index] = cycle[:index] if @completion_overlay
|
|
671
|
+
cycle[:input] = composer_input
|
|
672
|
+
cycle[:cursor] = composer_cursor
|
|
638
673
|
end
|
|
639
674
|
|
|
640
675
|
def handle_tab_key_binding(key)
|
|
@@ -710,7 +745,7 @@ module Kward
|
|
|
710
745
|
when TerminalKeys::CTRL_K
|
|
711
746
|
kill_line_after_cursor
|
|
712
747
|
when TerminalKeys::CTRL_L
|
|
713
|
-
|
|
748
|
+
request_transcript_redraw
|
|
714
749
|
when TerminalKeys::CTRL_U
|
|
715
750
|
kill_line_before_cursor
|
|
716
751
|
when TerminalKeys::CTRL_W
|