kward 0.83.0 → 0.84.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 +68 -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/api.md +4 -0
- data/doc/composer.md +1 -1
- data/doc/configuration.md +68 -21
- data/doc/editor.md +28 -13
- data/doc/files.md +8 -4
- data/doc/getting-started.md +3 -0
- data/doc/pan.md +19 -15
- data/doc/platform-support.md +48 -0
- data/doc/security.md +3 -2
- data/doc/shell.md +62 -45
- data/doc/troubleshooting.md +12 -2
- data/doc/usage.md +5 -5
- data/kward.gemspec +5 -4
- data/lib/kward/agent.rb +6 -3
- data/lib/kward/ansi.rb +110 -10
- data/lib/kward/cli/auth_commands.rb +34 -13
- data/lib/kward/cli/commands.rb +83 -62
- 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/project_skills_commands.rb +8 -4
- data/lib/kward/cli/prompt_interface.rb +27 -0
- data/lib/kward/cli/rendering.rb +15 -9
- data/lib/kward/cli/runtime_helpers.rb +118 -55
- data/lib/kward/cli/slash_commands.rb +12 -14
- data/lib/kward/cli/tabs.rb +83 -12
- data/lib/kward/cli/tool_summaries.rb +14 -0
- data/lib/kward/cli.rb +45 -7
- data/lib/kward/cli_transcript_formatter.rb +11 -4
- data/lib/kward/config_files.rb +82 -68
- data/lib/kward/detached_run.rb +44 -0
- data/lib/kward/interactive_pty_runner.rb +102 -28
- data/lib/kward/{ekwsh.rb → kwsh.rb} +35 -16
- data/lib/kward/kwshrc.rb +233 -0
- data/lib/kward/markdown_code_block.rb +136 -0
- data/lib/kward/model/client.rb +34 -22
- data/lib/kward/model/provider_catalog.rb +5 -0
- data/lib/kward/model/stream_parser.rb +20 -4
- data/lib/kward/pan/index.html.erb +3 -3
- data/lib/kward/pan/server.rb +23 -3
- data/lib/kward/persistent_shell_session.rb +119 -26
- data/lib/kward/project_files.rb +2 -2
- 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/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/state.rb +10 -10
- 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/overlay_renderer.rb +7 -5
- data/lib/kward/prompt_interface/project_browser.rb +415 -14
- data/lib/kward/prompt_interface/runtime_state.rb +50 -1
- data/lib/kward/prompt_interface/screen.rb +2 -2
- 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 +93 -18
- data/lib/kward/prompts/commands.rb +1 -1
- data/lib/kward/prompts.rb +1 -1
- data/lib/kward/pty_output_sink.rb +47 -0
- data/lib/kward/rpc/transcript_normalizer.rb +7 -3
- data/lib/kward/scratchpad_languages.rb +74 -0
- data/lib/kward/scratchpad_runner.rb +155 -29
- data/lib/kward/shell_prompt.rb +2 -0
- data/lib/kward/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/open_editor.rb +41 -0
- data/lib/kward/tools/registry.rb +13 -4
- data/lib/kward/tools/tool_call.rb +2 -1
- data/lib/kward/version.rb +1 -1
- data/templates/default/fulldoc/html/css/kward.css +0 -125
- data/templates/default/fulldoc/html/images/kward_workflow.svg +52 -0
- data/templates/default/fulldoc/html/setup.rb +1 -1
- 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 +6 -18
- data/templates/default/layout/html/setup.rb +41 -2
- metadata +36 -8
- data/templates/default/fulldoc/html/images/kward_screen_1.png +0 -0
data/lib/kward/cli/tabs.rb
CHANGED
|
@@ -33,9 +33,11 @@ module Kward
|
|
|
33
33
|
:markdown_chunks,
|
|
34
34
|
:label,
|
|
35
35
|
:unread,
|
|
36
|
+
:attention,
|
|
36
37
|
:pending_question,
|
|
37
38
|
:shell,
|
|
38
39
|
:shell_agent,
|
|
40
|
+
:background_run,
|
|
39
41
|
:transient_shell_entries,
|
|
40
42
|
:error_reported,
|
|
41
43
|
:local_busy_activity,
|
|
@@ -106,6 +108,10 @@ module Kward
|
|
|
106
108
|
@cli.send(:ask_tab_user_question, @tab, questions, cancellation: cancellation)
|
|
107
109
|
end
|
|
108
110
|
|
|
111
|
+
def edit_file(path, base_dir:, allow_new:)
|
|
112
|
+
@cli.send(:open_editor_for_agent, path, base_dir: base_dir, allow_new: allow_new)
|
|
113
|
+
end
|
|
114
|
+
|
|
109
115
|
def ask_tool_approval(tool_name:, args:, reason: nil)
|
|
110
116
|
answers = ask_user_question([
|
|
111
117
|
{
|
|
@@ -316,9 +322,11 @@ module Kward
|
|
|
316
322
|
markdown_chunks: [],
|
|
317
323
|
label: label,
|
|
318
324
|
unread: false,
|
|
325
|
+
attention: nil,
|
|
319
326
|
pending_question: nil,
|
|
320
327
|
shell: nil,
|
|
321
328
|
shell_agent: nil,
|
|
329
|
+
background_run: nil,
|
|
322
330
|
transient_shell_entries: [],
|
|
323
331
|
error_reported: false,
|
|
324
332
|
local_busy_activity: nil
|
|
@@ -420,14 +428,14 @@ module Kward
|
|
|
420
428
|
end
|
|
421
429
|
|
|
422
430
|
if @tabs.length <= 1
|
|
423
|
-
|
|
431
|
+
close_kwsh_session(tab.shell) if tab&.shell && respond_to?(:close_kwsh_session, true)
|
|
424
432
|
@tabs.clear
|
|
425
433
|
persist_tabs
|
|
426
434
|
return PromptInterface::EXIT_INPUT
|
|
427
435
|
end
|
|
428
436
|
|
|
429
437
|
stop_tab_live_view
|
|
430
|
-
|
|
438
|
+
close_kwsh_session(tab.shell) if tab&.shell && respond_to?(:close_kwsh_session, true)
|
|
431
439
|
tab.session&.delete_if_unused if tab&.session.respond_to?(:delete_if_unused)
|
|
432
440
|
tab.driver.close if tab.driver.respond_to?(:close)
|
|
433
441
|
@tabs.delete_at(@active_tab_index)
|
|
@@ -461,12 +469,13 @@ module Kward
|
|
|
461
469
|
tab.error = nil
|
|
462
470
|
tab.answer = nil
|
|
463
471
|
tab.unread = false
|
|
472
|
+
tab.attention = nil
|
|
464
473
|
tab.error_reported = false
|
|
465
474
|
tab.event_history.clear
|
|
466
475
|
tab.seen_events = 0
|
|
467
476
|
tab.queued_inputs.clear
|
|
468
477
|
tab.steering = nil
|
|
469
|
-
|
|
478
|
+
close_kwsh_session(tab.shell) if tab.shell && respond_to?(:close_kwsh_session, true)
|
|
470
479
|
tab.shell = nil
|
|
471
480
|
tab.shell_agent = nil
|
|
472
481
|
tab.clear_transient_shell_entries
|
|
@@ -501,6 +510,7 @@ module Kward
|
|
|
501
510
|
end
|
|
502
511
|
|
|
503
512
|
def activate_tab(index, render: true)
|
|
513
|
+
service_background_tab_runs
|
|
504
514
|
tab = @tabs[index]
|
|
505
515
|
return nil unless tab
|
|
506
516
|
|
|
@@ -517,6 +527,7 @@ module Kward
|
|
|
517
527
|
@prompt.update_assistant_label(assistant_prompt_name) if @prompt.respond_to?(:update_assistant_label)
|
|
518
528
|
end
|
|
519
529
|
tab.unread = false
|
|
530
|
+
tab.attention = nil
|
|
520
531
|
restore_tab_composer_snapshot(tab.snapshot)
|
|
521
532
|
update_prompt_workspace_root(current_workspace_root)
|
|
522
533
|
@prompt.refresh_composer_status if @prompt.respond_to?(:refresh_composer_status)
|
|
@@ -603,6 +614,44 @@ module Kward
|
|
|
603
614
|
index ? index + 1 : active_tab_number
|
|
604
615
|
end
|
|
605
616
|
|
|
617
|
+
def register_background_tab_run(tab, run, &completion)
|
|
618
|
+
return unless tab && run
|
|
619
|
+
|
|
620
|
+
tab.background_run = { run: run, completion: completion }
|
|
621
|
+
tab.local_busy_activity = "running"
|
|
622
|
+
tab.attention = nil
|
|
623
|
+
update_prompt_tabs
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
def service_background_tab_runs
|
|
627
|
+
return unless @tabs
|
|
628
|
+
|
|
629
|
+
@tabs.each do |tab|
|
|
630
|
+
state = tab.background_run
|
|
631
|
+
next unless state&.fetch(:run)&.complete?
|
|
632
|
+
|
|
633
|
+
result = state[:run].result
|
|
634
|
+
tab.background_run = nil
|
|
635
|
+
state[:completion]&.call(state[:run].sink, result) if result.is_a?(Struct)
|
|
636
|
+
finish_local_busy_command_for_tab(tab)
|
|
637
|
+
if tab != active_tab
|
|
638
|
+
tab.unread = true
|
|
639
|
+
tab.attention = :success
|
|
640
|
+
end
|
|
641
|
+
rescue StandardError => e
|
|
642
|
+
tab.background_run = nil
|
|
643
|
+
tab.error = e.message
|
|
644
|
+
finish_local_busy_command_for_tab(tab)
|
|
645
|
+
tab.attention = :failure if tab != active_tab
|
|
646
|
+
end
|
|
647
|
+
update_prompt_tabs
|
|
648
|
+
end
|
|
649
|
+
|
|
650
|
+
def finish_local_busy_command_for_tab(tab)
|
|
651
|
+
tab.local_busy_activity = nil
|
|
652
|
+
@prompt.finish_busy_input if prompt_interface? && tab == active_tab && @prompt.respond_to?(:finish_busy_input)
|
|
653
|
+
end
|
|
654
|
+
|
|
606
655
|
def submit_tab_input(tab, input, display_input: nil)
|
|
607
656
|
return if input.to_s.strip.empty?
|
|
608
657
|
|
|
@@ -616,6 +665,7 @@ module Kward
|
|
|
616
665
|
print_user_transcript(input, display_input: display_input) if prompt_interface?
|
|
617
666
|
tab.status = "queued"
|
|
618
667
|
tab.unread = false
|
|
668
|
+
tab.attention = nil
|
|
619
669
|
tab.cancellation = Cancellation.new
|
|
620
670
|
tab.steering = tab.driver.supports_steering? && steering_supported? ? Steering.new : nil
|
|
621
671
|
tab.error = nil
|
|
@@ -639,14 +689,17 @@ module Kward
|
|
|
639
689
|
end
|
|
640
690
|
tab.status = "ready"
|
|
641
691
|
tab.unread = tab != active_tab
|
|
692
|
+
tab.attention = :output if tab != active_tab
|
|
642
693
|
rescue Cancellation::CancelledError
|
|
643
694
|
tab.status = "cancelled"
|
|
644
695
|
tab.unread = false
|
|
696
|
+
tab.attention = :failure if tab != active_tab
|
|
645
697
|
report_tab_runtime_error(tab)
|
|
646
698
|
rescue StandardError => e
|
|
647
699
|
tab.error = e.message
|
|
648
700
|
tab.status = "failed"
|
|
649
701
|
tab.unread = false
|
|
702
|
+
tab.attention = :failure if tab != active_tab
|
|
650
703
|
report_tab_runtime_error(tab)
|
|
651
704
|
ensure
|
|
652
705
|
finish_tab_turn(tab)
|
|
@@ -662,8 +715,9 @@ module Kward
|
|
|
662
715
|
end
|
|
663
716
|
|
|
664
717
|
def poll_active_tab_input
|
|
718
|
+
service_background_tab_runs
|
|
665
719
|
tab = active_tab
|
|
666
|
-
unless tab&.running?
|
|
720
|
+
unless tab&.running? || tab&.local_busy?
|
|
667
721
|
input = @prompt.ask("You>")
|
|
668
722
|
return { tab_action: :close } if input.nil? && tab && @tabs.length > 1
|
|
669
723
|
|
|
@@ -672,6 +726,7 @@ module Kward
|
|
|
672
726
|
|
|
673
727
|
@prompt.begin_busy_input("You>") if @prompt.respond_to?(:begin_busy_input)
|
|
674
728
|
loop do
|
|
729
|
+
service_background_tab_runs
|
|
675
730
|
refresh_active_tab
|
|
676
731
|
if service_active_tab_question
|
|
677
732
|
return next_tab_queued_input(tab) if tab.idle? && !tab.queued_inputs.empty?
|
|
@@ -679,6 +734,10 @@ module Kward
|
|
|
679
734
|
|
|
680
735
|
next
|
|
681
736
|
end
|
|
737
|
+
if @prompt.respond_to?(:modal_active?) && @prompt.modal_active?
|
|
738
|
+
sleep 0.01
|
|
739
|
+
next
|
|
740
|
+
end
|
|
682
741
|
poll_result = @prompt.poll_input
|
|
683
742
|
case poll_result
|
|
684
743
|
when Hash
|
|
@@ -687,8 +746,12 @@ module Kward
|
|
|
687
746
|
return poll_result
|
|
688
747
|
end
|
|
689
748
|
when PromptInterface::CANCEL_INPUT
|
|
690
|
-
tab.
|
|
691
|
-
|
|
749
|
+
if tab.background_run
|
|
750
|
+
tab.background_run[:run].cancel!
|
|
751
|
+
else
|
|
752
|
+
tab.cancellation&.cancel!
|
|
753
|
+
tab.thread&.raise(Cancellation::CancelledError, "cancelled") if tab.thread&.alive?
|
|
754
|
+
end
|
|
692
755
|
when PromptInterface::EXIT_INPUT
|
|
693
756
|
tab.queued_inputs << "/exit"
|
|
694
757
|
@prompt.set_queued_count(tab.queued_inputs.length) if @prompt.respond_to?(:set_queued_count)
|
|
@@ -830,15 +893,17 @@ module Kward
|
|
|
830
893
|
return if tab.stream_state[:streamed]
|
|
831
894
|
return if tab.answer.to_s.empty?
|
|
832
895
|
|
|
833
|
-
@prompt.say("\n#{colored(
|
|
896
|
+
@prompt.say("\n#{colored(transcript_marker("Assistant"), :activity, :bold)} #{render_markdown_transcript(tab.answer)}\n")
|
|
834
897
|
end
|
|
835
898
|
|
|
836
899
|
def new_tab_stream_state(driver)
|
|
837
900
|
{
|
|
838
901
|
streamed: false,
|
|
902
|
+
assistant_arrived: false,
|
|
839
903
|
last_flush: monotonic_now,
|
|
840
904
|
stream_block_open: false,
|
|
841
905
|
markdown_streams: {},
|
|
906
|
+
tool_elapsed_ms: {},
|
|
842
907
|
defer_assistant_streaming: driver.respond_to?(:conversation) && defer_assistant_streaming?(driver.agent)
|
|
843
908
|
}
|
|
844
909
|
end
|
|
@@ -853,10 +918,11 @@ module Kward
|
|
|
853
918
|
end
|
|
854
919
|
|
|
855
920
|
def tab_label_color(tab)
|
|
856
|
-
return :
|
|
857
|
-
return :
|
|
858
|
-
return :
|
|
859
|
-
return :
|
|
921
|
+
return :success if tab.status.to_s == "waiting_for_question"
|
|
922
|
+
return :caution if tab.running? || tab.local_busy?
|
|
923
|
+
return :failure if %w[failed cancelled].include?(tab.status.to_s) || tab.attention == :failure
|
|
924
|
+
return :success if tab.attention == :success
|
|
925
|
+
return :activity if tab.unread
|
|
860
926
|
|
|
861
927
|
nil
|
|
862
928
|
end
|
|
@@ -970,7 +1036,12 @@ module Kward
|
|
|
970
1036
|
tab.thread&.raise(Cancellation::CancelledError, "cancelled") if tab.thread&.alive?
|
|
971
1037
|
tab.thread&.join(0.2)
|
|
972
1038
|
end
|
|
973
|
-
|
|
1039
|
+
if (background_run = tab.background_run&.fetch(:run))
|
|
1040
|
+
background_run.cancel!
|
|
1041
|
+
background_run.join(0.5)
|
|
1042
|
+
end
|
|
1043
|
+
tab.background_run = nil
|
|
1044
|
+
close_kwsh_session(tab.shell) if tab&.shell && respond_to?(:close_kwsh_session, true)
|
|
974
1045
|
rescue StandardError
|
|
975
1046
|
nil
|
|
976
1047
|
end
|
|
@@ -6,6 +6,20 @@ module Kward
|
|
|
6
6
|
module ToolSummaries
|
|
7
7
|
private
|
|
8
8
|
|
|
9
|
+
def tool_activity(tool_call)
|
|
10
|
+
"running #{tool_call_name(tool_call).tr("_", " ")}"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def tool_summary_with_duration(summary, elapsed_ms)
|
|
14
|
+
return summary.to_s unless elapsed_ms
|
|
15
|
+
|
|
16
|
+
lines = summary.to_s.lines
|
|
17
|
+
first = lines.shift.to_s.chomp
|
|
18
|
+
duration = elapsed_ms < 1_000 ? "#{elapsed_ms.round} ms" : format("%.1f s", elapsed_ms / 1_000.0)
|
|
19
|
+
first = "#{first} · #{duration}"
|
|
20
|
+
lines.empty? ? first : (["#{first}\n"] + lines).join
|
|
21
|
+
end
|
|
22
|
+
|
|
9
23
|
def tool_result_summary(tool_call, content)
|
|
10
24
|
name = tool_call_name(tool_call)
|
|
11
25
|
args = tool_call_args(tool_call)
|
data/lib/kward/cli.rb
CHANGED
|
@@ -18,7 +18,7 @@ require_relative "cli_transcript_formatter"
|
|
|
18
18
|
require_relative "model/context_usage"
|
|
19
19
|
require_relative "events"
|
|
20
20
|
require_relative "export_path"
|
|
21
|
-
require_relative "
|
|
21
|
+
require_relative "kwsh"
|
|
22
22
|
require_relative "persistent_shell_session"
|
|
23
23
|
require_relative "interactive_pty_runner"
|
|
24
24
|
require_relative "adaptive_pty_output_sink"
|
|
@@ -92,7 +92,7 @@ module Kward
|
|
|
92
92
|
RESTORED_TOOL_OUTPUT_LIMIT = 2_000
|
|
93
93
|
INTERACTIVE_TOOL_OUTPUT_LINE_LIMIT = 10
|
|
94
94
|
STREAM_RENDER_INTERVAL = 0.025
|
|
95
|
-
INTERACTIVE_EVENT_DRAIN_LIMIT =
|
|
95
|
+
INTERACTIVE_EVENT_DRAIN_LIMIT = 12
|
|
96
96
|
BUILTIN_SLASH_COMMANDS = PromptCommands::BUILTIN_COMMANDS
|
|
97
97
|
BUILTIN_SLASH_COMMAND_NAMES = PromptCommands::BUILTIN_RESERVED_COMMAND_NAMES
|
|
98
98
|
|
|
@@ -156,6 +156,16 @@ module Kward
|
|
|
156
156
|
warn e.message
|
|
157
157
|
warn "Run `kward help` for available commands."
|
|
158
158
|
exit 1
|
|
159
|
+
rescue Client::AuthenticationError => e
|
|
160
|
+
raise if debug_errors?
|
|
161
|
+
|
|
162
|
+
warn authentication_error_message(e)
|
|
163
|
+
exit 1
|
|
164
|
+
rescue StandardError => e
|
|
165
|
+
raise if debug_errors?
|
|
166
|
+
|
|
167
|
+
warn runtime_error_message(e)
|
|
168
|
+
exit 1
|
|
159
169
|
ensure
|
|
160
170
|
ConfigFiles.skip_config = false
|
|
161
171
|
end
|
|
@@ -184,6 +194,30 @@ module Kward
|
|
|
184
194
|
MESSAGE
|
|
185
195
|
end
|
|
186
196
|
|
|
197
|
+
def authentication_error_message(error)
|
|
198
|
+
<<~MESSAGE.rstrip
|
|
199
|
+
Authentication required
|
|
200
|
+
|
|
201
|
+
#{error.message}
|
|
202
|
+
|
|
203
|
+
Run `kward doctor` to inspect your setup.
|
|
204
|
+
MESSAGE
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def runtime_error_message(error)
|
|
208
|
+
<<~MESSAGE.rstrip
|
|
209
|
+
Kward could not complete the request.
|
|
210
|
+
|
|
211
|
+
#{error.message}
|
|
212
|
+
|
|
213
|
+
Set KWARD_DEBUG=1 to show a backtrace.
|
|
214
|
+
MESSAGE
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def debug_errors?
|
|
218
|
+
ENV["KWARD_DEBUG"] == "1"
|
|
219
|
+
end
|
|
220
|
+
|
|
187
221
|
def dispatch
|
|
188
222
|
if @prompt_delimited
|
|
189
223
|
ConfigFiles.ensure_default_config!
|
|
@@ -229,7 +263,7 @@ module Kward
|
|
|
229
263
|
raise ArgumentError, command_usage("doctor") unless @argv.length == 1
|
|
230
264
|
|
|
231
265
|
ensure_client!
|
|
232
|
-
print_doctor
|
|
266
|
+
exit 1 unless print_doctor
|
|
233
267
|
return
|
|
234
268
|
end
|
|
235
269
|
|
|
@@ -239,7 +273,7 @@ module Kward
|
|
|
239
273
|
return
|
|
240
274
|
end
|
|
241
275
|
|
|
242
|
-
handle_hooks_command((@argv[1..] || []).join(" "))
|
|
276
|
+
handle_hooks_command((@argv[1..] || []).join(" "), command_line: true)
|
|
243
277
|
return
|
|
244
278
|
end
|
|
245
279
|
|
|
@@ -478,8 +512,9 @@ module Kward
|
|
|
478
512
|
install_bang_completion_provider(agent)
|
|
479
513
|
|
|
480
514
|
loop do
|
|
481
|
-
if
|
|
482
|
-
|
|
515
|
+
service_background_tab_runs if respond_to?(:service_background_tab_runs, true)
|
|
516
|
+
if @pending_inputs.empty? && active_tab&.shell && !active_tab.local_busy?
|
|
517
|
+
run_kwsh_loop(active_tab.shell, tab: active_tab, history: build_kwsh_history(active_tab.agent))
|
|
483
518
|
end
|
|
484
519
|
input = @pending_inputs.shift || (active_tab ? poll_active_tab_input : @prompt.ask("You>"))
|
|
485
520
|
if input.is_a?(Hash) && input[:editor_prompt]
|
|
@@ -537,7 +572,10 @@ module Kward
|
|
|
537
572
|
end
|
|
538
573
|
end
|
|
539
574
|
next if handled
|
|
540
|
-
|
|
575
|
+
if shell_command_input?(command_input)
|
|
576
|
+
shell_result = handle_interactive_shell_command(command_input, agent)
|
|
577
|
+
next if shell_result == true || shell_result == :tab_action
|
|
578
|
+
end
|
|
541
579
|
|
|
542
580
|
flush_pending_reasoning_config(conversation: active_tab.agent.conversation) if active_tab&.agent
|
|
543
581
|
flush_pending_reasoning_config(conversation: agent.conversation) unless active_tab
|
|
@@ -15,10 +15,17 @@ module Kward
|
|
|
15
15
|
|
|
16
16
|
def reasoning_blocks(message)
|
|
17
17
|
response_blocks = response_items(message).flat_map do |item|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
case MessageAccess.value(item, :type)
|
|
19
|
+
when "reasoning"
|
|
20
|
+
summary = response_item_text_parts(MessageAccess.value(item, :summary))
|
|
21
|
+
summary.empty? ? response_item_text_parts(MessageAccess.value(item, :content)) : summary
|
|
22
|
+
when "message"
|
|
23
|
+
next [] unless MessageAccess.value(item, :phase).to_s == "commentary"
|
|
24
|
+
|
|
25
|
+
response_item_text_parts(MessageAccess.value(item, :content))
|
|
26
|
+
else
|
|
27
|
+
[]
|
|
28
|
+
end
|
|
22
29
|
end
|
|
23
30
|
return response_blocks unless response_blocks.empty?
|
|
24
31
|
|
data/lib/kward/config_files.rb
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
require "digest"
|
|
2
2
|
require "fileutils"
|
|
3
3
|
require "json"
|
|
4
|
-
require "yaml"
|
|
5
4
|
require_relative "frontmatter"
|
|
6
5
|
require_relative "deep_copy"
|
|
7
6
|
require_relative "private_file"
|
|
8
7
|
require_relative "path_guard"
|
|
9
8
|
require_relative "permissions/policy"
|
|
10
9
|
require_relative "sandbox/policy"
|
|
11
|
-
require_relative "
|
|
10
|
+
require_relative "kwsh"
|
|
11
|
+
require_relative "kwshrc"
|
|
12
12
|
require_relative "editor_mode"
|
|
13
13
|
require_relative "diff_view_mode"
|
|
14
14
|
require_relative "prompts/templates"
|
|
@@ -107,9 +107,14 @@ module Kward
|
|
|
107
107
|
File.join(config_dir, "cache")
|
|
108
108
|
end
|
|
109
109
|
|
|
110
|
-
# @return [String] embedded-shell
|
|
111
|
-
def
|
|
112
|
-
File.join(config_dir, "
|
|
110
|
+
# @return [String] embedded-shell rc config path
|
|
111
|
+
def kwshrc_path
|
|
112
|
+
File.join(config_dir, "kwshrc")
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# @return [Array<String>] embedded-shell rc config paths in load order
|
|
116
|
+
def kwshrc_paths
|
|
117
|
+
[kwshrc_path, File.expand_path("~/.kwshrc")].uniq
|
|
113
118
|
end
|
|
114
119
|
|
|
115
120
|
def workspace_hooks_path(root = Dir.pwd)
|
|
@@ -137,6 +142,7 @@ module Kward
|
|
|
137
142
|
"editor" => {
|
|
138
143
|
"mode" => "modern",
|
|
139
144
|
"agent" => {},
|
|
145
|
+
"runners" => {},
|
|
140
146
|
"auto_indent" => true,
|
|
141
147
|
"auto_close_pairs" => true,
|
|
142
148
|
"soft_wrap" => true,
|
|
@@ -340,67 +346,25 @@ module Kward
|
|
|
340
346
|
merged
|
|
341
347
|
end
|
|
342
348
|
|
|
343
|
-
def
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
end
|
|
352
|
-
|
|
353
|
-
def normalize_ekwsh_config(data)
|
|
354
|
-
data = data.transform_keys(&:to_s) if data.is_a?(Hash)
|
|
355
|
-
settings = data.is_a?(Hash) ? data : {}
|
|
356
|
-
{
|
|
357
|
-
shell: normalize_ekwsh_shell(settings["shell"]),
|
|
358
|
-
timeout_seconds: normalize_positive_integer(settings["timeout_seconds"], Ekwsh::DEFAULT_TIMEOUT_SECONDS),
|
|
359
|
-
max_output_bytes: normalize_positive_integer(settings["max_output_bytes"], Ekwsh::DEFAULT_MAX_OUTPUT_BYTES),
|
|
360
|
-
history_limit: normalize_positive_integer(settings["history_limit"], Ekwsh::DEFAULT_HISTORY_LIMIT),
|
|
361
|
-
env: normalize_ekwsh_env(settings["env"]),
|
|
362
|
-
aliases: normalize_ekwsh_aliases(settings["aliases"])
|
|
349
|
+
def read_kwsh_config
|
|
350
|
+
config = {
|
|
351
|
+
shell: Kwsh::DEFAULT_SHELL,
|
|
352
|
+
timeout_seconds: Kwsh::DEFAULT_TIMEOUT_SECONDS,
|
|
353
|
+
max_output_bytes: Kwsh::DEFAULT_MAX_OUTPUT_BYTES,
|
|
354
|
+
history_limit: Kwsh::DEFAULT_HISTORY_LIMIT,
|
|
355
|
+
env: {},
|
|
356
|
+
aliases: {}
|
|
363
357
|
}
|
|
358
|
+
rc_config = read_kwshrc_config
|
|
359
|
+
config[:env].merge!(rc_config[:env])
|
|
360
|
+
config[:aliases].merge!(rc_config[:aliases])
|
|
361
|
+
config
|
|
364
362
|
end
|
|
365
363
|
|
|
366
|
-
def
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
Ekwsh::DEFAULT_SHELL
|
|
372
|
-
end
|
|
373
|
-
|
|
374
|
-
def normalize_positive_integer(value, default)
|
|
375
|
-
integer = Integer(value)
|
|
376
|
-
integer.positive? ? integer : default
|
|
377
|
-
rescue ArgumentError, TypeError
|
|
378
|
-
default
|
|
379
|
-
end
|
|
380
|
-
|
|
381
|
-
def normalize_ekwsh_env(values)
|
|
382
|
-
return {} unless values.is_a?(Hash)
|
|
383
|
-
|
|
384
|
-
values.each_with_object({}) do |(key, value), result|
|
|
385
|
-
key = key.to_s
|
|
386
|
-
next unless key.match?(/\A[A-Za-z_][A-Za-z0-9_]*\z/)
|
|
387
|
-
next if value.nil?
|
|
388
|
-
|
|
389
|
-
result[key] = value.to_s
|
|
390
|
-
end
|
|
391
|
-
end
|
|
392
|
-
|
|
393
|
-
def normalize_ekwsh_aliases(values)
|
|
394
|
-
return {} unless values.is_a?(Hash)
|
|
395
|
-
|
|
396
|
-
values.each_with_object({}) do |(name, command), result|
|
|
397
|
-
name = name.to_s
|
|
398
|
-
command = command.to_s.strip
|
|
399
|
-
next unless Ekwsh.valid_alias_name?(name)
|
|
400
|
-
next if command.empty?
|
|
401
|
-
|
|
402
|
-
result[name] = command
|
|
403
|
-
end
|
|
364
|
+
def read_kwshrc_config(paths = kwshrc_paths, env: ENV.to_h)
|
|
365
|
+
Kwshrc.read(paths, env: env)
|
|
366
|
+
rescue Kwshrc::ParseError => e
|
|
367
|
+
raise "Invalid kwshrc config: #{e.message}"
|
|
404
368
|
end
|
|
405
369
|
|
|
406
370
|
# Merges top-level config values and writes the updated config privately.
|
|
@@ -483,19 +447,69 @@ module Kward
|
|
|
483
447
|
EditorMode.normalize(editor["mode"])
|
|
484
448
|
end
|
|
485
449
|
|
|
450
|
+
# Returns configured editor runner definitions, or an empty map.
|
|
451
|
+
#
|
|
452
|
+
# Runner defaults are resolved by the editor runner at execution time so
|
|
453
|
+
# the generated config remains portable across machines.
|
|
454
|
+
def editor_runners(config = read_config)
|
|
455
|
+
editor = config["editor"].is_a?(Hash) ? config["editor"] : {}
|
|
456
|
+
runners = editor["runners"]
|
|
457
|
+
runners.is_a?(Hash) ? runners : {}
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
# Returns the optional provider override for editor-agent turns.
|
|
461
|
+
def editor_agent_provider(config = read_config)
|
|
462
|
+
value = ENV["KWARD_EDITOR_PROVIDER"].to_s.strip
|
|
463
|
+
return value unless value.empty?
|
|
464
|
+
|
|
465
|
+
editor_agent_setting(config, "provider")
|
|
466
|
+
end
|
|
467
|
+
|
|
486
468
|
# Returns the optional model override for editor-agent turns.
|
|
487
469
|
def editor_agent_model(config = read_config)
|
|
488
|
-
|
|
489
|
-
agent = editor["agent"].is_a?(Hash) ? editor["agent"] : {}
|
|
490
|
-
value = agent["model"].to_s.strip
|
|
491
|
-
value.empty? ? nil : value
|
|
470
|
+
editor_agent_setting(config, "model")
|
|
492
471
|
end
|
|
493
472
|
|
|
494
473
|
# Returns the optional reasoning-effort override for editor-agent turns.
|
|
495
474
|
def editor_agent_reasoning_effort(config = read_config)
|
|
475
|
+
editor_agent_setting(config, "reasoning_effort")
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
# Returns the shell-agent provider override, preferring the environment.
|
|
479
|
+
def shell_agent_provider(config = read_config)
|
|
480
|
+
value = ENV["KWSH_PROVIDER"].to_s.strip
|
|
481
|
+
return value unless value.empty?
|
|
482
|
+
|
|
483
|
+
shell_agent_setting(config, "provider")
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
# Returns the shell-agent model override, preferring the environment.
|
|
487
|
+
def shell_agent_model(config = read_config)
|
|
488
|
+
value = ENV["KWSH_MODE"].to_s.strip
|
|
489
|
+
return value unless value.empty?
|
|
490
|
+
|
|
491
|
+
shell_agent_setting(config, "model")
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
# Returns the shell-agent reasoning-effort override, preferring the environment.
|
|
495
|
+
def shell_agent_reasoning_effort(config = read_config)
|
|
496
|
+
value = ENV["KWSH_REASONING"].to_s.strip
|
|
497
|
+
return value unless value.empty?
|
|
498
|
+
|
|
499
|
+
shell_agent_setting(config, "reasoning_effort")
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
def editor_agent_setting(config, key)
|
|
496
503
|
editor = config["editor"].is_a?(Hash) ? config["editor"] : {}
|
|
497
504
|
agent = editor["agent"].is_a?(Hash) ? editor["agent"] : {}
|
|
498
|
-
value = agent[
|
|
505
|
+
value = agent[key].to_s.strip
|
|
506
|
+
value.empty? ? nil : value
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
def shell_agent_setting(config, key)
|
|
510
|
+
shell = config["shell"].is_a?(Hash) ? config["shell"] : {}
|
|
511
|
+
agent = shell["agent"].is_a?(Hash) ? shell["agent"] : {}
|
|
512
|
+
value = agent[key].to_s.strip
|
|
499
513
|
value.empty? ? nil : value
|
|
500
514
|
end
|
|
501
515
|
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
require "thread"
|
|
2
|
+
|
|
3
|
+
# Namespace for the Kward CLI agent runtime.
|
|
4
|
+
module Kward
|
|
5
|
+
# Owns a command that continues after its terminal has been returned to Kward.
|
|
6
|
+
class DetachedRun
|
|
7
|
+
attr_reader :sink
|
|
8
|
+
|
|
9
|
+
def initialize(sink:, canceler: nil, &work)
|
|
10
|
+
@sink = sink
|
|
11
|
+
@canceler = canceler
|
|
12
|
+
@mutex = Mutex.new
|
|
13
|
+
@result = nil
|
|
14
|
+
@thread = Thread.new do
|
|
15
|
+
begin
|
|
16
|
+
result = work.call
|
|
17
|
+
@mutex.synchronize { @result = result }
|
|
18
|
+
rescue StandardError => e
|
|
19
|
+
@mutex.synchronize { @result = e }
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
@thread.report_on_exception = false
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def complete?
|
|
26
|
+
!@thread.alive?
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def result
|
|
30
|
+
return unless complete?
|
|
31
|
+
|
|
32
|
+
@thread.join
|
|
33
|
+
@mutex.synchronize { @result }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def join(timeout = nil)
|
|
37
|
+
@thread.join(timeout)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def cancel!
|
|
41
|
+
@canceler&.call
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|