kward 0.82.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 +96 -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 +1 -0
- data/doc/api.md +4 -0
- data/doc/composer.md +2 -2
- data/doc/configuration.md +82 -20
- data/doc/editor.md +47 -22
- data/doc/files.md +12 -7
- data/doc/getting-started.md +3 -0
- data/doc/pan.md +19 -15
- data/doc/permissions.md +1 -0
- data/doc/platform-support.md +48 -0
- data/doc/releasing.md +12 -4
- data/doc/rpc.md +2 -2
- data/doc/sandboxing.md +5 -1
- data/doc/security.md +4 -3
- data/doc/shell.md +101 -60
- data/doc/tabs.md +3 -0
- data/doc/troubleshooting.md +12 -2
- data/doc/usage.md +7 -6
- data/kward.gemspec +5 -4
- data/lib/kward/agent.rb +7 -3
- data/lib/kward/ansi.rb +110 -10
- data/lib/kward/auth/anthropic_oauth.rb +7 -7
- 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 +144 -21
- 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 +260 -55
- data/lib/kward/cli/slash_commands.rb +21 -12
- data/lib/kward/cli/tabs.rb +91 -13
- data/lib/kward/cli/tool_summaries.rb +14 -0
- data/lib/kward/cli.rb +55 -7
- data/lib/kward/cli_transcript_formatter.rb +11 -4
- data/lib/kward/compaction/token_estimator.rb +12 -6
- data/lib/kward/config_files.rb +94 -63
- data/lib/kward/detached_run.rb +44 -0
- data/lib/kward/editor_prompt.rb +46 -0
- data/lib/kward/editor_prompt_session.rb +28 -0
- data/lib/kward/interactive_pty_runner.rb +102 -28
- data/lib/kward/{ekwsh.rb → kwsh.rb} +103 -24
- data/lib/kward/kwshrc.rb +233 -0
- data/lib/kward/markdown_code_block.rb +136 -0
- data/lib/kward/model/client.rb +35 -39
- data/lib/kward/model/model_info.rb +3 -2
- data/lib/kward/model/payloads.rb +0 -2
- 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 +843 -0
- data/lib/kward/project_files.rb +19 -6
- data/lib/kward/prompt_interface/composer_renderer.rb +45 -41
- data/lib/kward/prompt_interface/composer_state.rb +33 -24
- data/lib/kward/prompt_interface/editor/auto_indent.rb +27 -22
- data/lib/kward/prompt_interface/editor/controller.rb +99 -33
- data/lib/kward/prompt_interface/editor/endwise.rb +13 -4
- data/lib/kward/prompt_interface/editor/modes/modern.rb +4 -0
- data/lib/kward/prompt_interface/editor/modes/vibe.rb +298 -47
- data/lib/kward/prompt_interface/editor/renderer.rb +109 -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 +21 -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/editor/word_completion.rb +124 -0
- data/lib/kward/prompt_interface/file_overlay.rb +92 -22
- data/lib/kward/prompt_interface/key_handler.rb +75 -0
- data/lib/kward/prompt_interface/overlay_renderer.rb +7 -5
- data/lib/kward/prompt_interface/project_browser.rb +443 -17
- data/lib/kward/prompt_interface/runtime_state.rb +55 -2
- 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 +169 -18
- data/lib/kward/prompts/commands.rb +2 -1
- data/lib/kward/prompts.rb +1 -1
- data/lib/kward/pty_output_sink.rb +47 -0
- data/lib/kward/rpc/auth_manager.rb +1 -1
- data/lib/kward/rpc/server.rb +2 -1
- 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 +52 -0
- data/lib/kward/shell_prompt_session.rb +58 -0
- data/lib/kward/terminal_keys.rb +13 -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/prepare_shell_command.rb +28 -0
- data/lib/kward/tools/registry.rb +75 -8
- data/lib/kward/tools/replace_editor_buffer.rb +30 -0
- data/lib/kward/tools/run_shell_command.rb +24 -6
- 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 +44 -8
- data/templates/default/fulldoc/html/images/kward_screen_1.png +0 -0
data/lib/kward/cli/tabs.rb
CHANGED
|
@@ -33,8 +33,11 @@ module Kward
|
|
|
33
33
|
:markdown_chunks,
|
|
34
34
|
:label,
|
|
35
35
|
:unread,
|
|
36
|
+
:attention,
|
|
36
37
|
:pending_question,
|
|
37
38
|
:shell,
|
|
39
|
+
:shell_agent,
|
|
40
|
+
:background_run,
|
|
38
41
|
:transient_shell_entries,
|
|
39
42
|
:error_reported,
|
|
40
43
|
:local_busy_activity,
|
|
@@ -105,6 +108,10 @@ module Kward
|
|
|
105
108
|
@cli.send(:ask_tab_user_question, @tab, questions, cancellation: cancellation)
|
|
106
109
|
end
|
|
107
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
|
+
|
|
108
115
|
def ask_tool_approval(tool_name:, args:, reason: nil)
|
|
109
116
|
answers = ask_user_question([
|
|
110
117
|
{
|
|
@@ -315,8 +322,11 @@ module Kward
|
|
|
315
322
|
markdown_chunks: [],
|
|
316
323
|
label: label,
|
|
317
324
|
unread: false,
|
|
325
|
+
attention: nil,
|
|
318
326
|
pending_question: nil,
|
|
319
327
|
shell: nil,
|
|
328
|
+
shell_agent: nil,
|
|
329
|
+
background_run: nil,
|
|
320
330
|
transient_shell_entries: [],
|
|
321
331
|
error_reported: false,
|
|
322
332
|
local_busy_activity: nil
|
|
@@ -418,12 +428,14 @@ module Kward
|
|
|
418
428
|
end
|
|
419
429
|
|
|
420
430
|
if @tabs.length <= 1
|
|
431
|
+
close_kwsh_session(tab.shell) if tab&.shell && respond_to?(:close_kwsh_session, true)
|
|
421
432
|
@tabs.clear
|
|
422
433
|
persist_tabs
|
|
423
434
|
return PromptInterface::EXIT_INPUT
|
|
424
435
|
end
|
|
425
436
|
|
|
426
437
|
stop_tab_live_view
|
|
438
|
+
close_kwsh_session(tab.shell) if tab&.shell && respond_to?(:close_kwsh_session, true)
|
|
427
439
|
tab.session&.delete_if_unused if tab&.session.respond_to?(:delete_if_unused)
|
|
428
440
|
tab.driver.close if tab.driver.respond_to?(:close)
|
|
429
441
|
@tabs.delete_at(@active_tab_index)
|
|
@@ -457,12 +469,15 @@ module Kward
|
|
|
457
469
|
tab.error = nil
|
|
458
470
|
tab.answer = nil
|
|
459
471
|
tab.unread = false
|
|
472
|
+
tab.attention = nil
|
|
460
473
|
tab.error_reported = false
|
|
461
474
|
tab.event_history.clear
|
|
462
475
|
tab.seen_events = 0
|
|
463
476
|
tab.queued_inputs.clear
|
|
464
477
|
tab.steering = nil
|
|
478
|
+
close_kwsh_session(tab.shell) if tab.shell && respond_to?(:close_kwsh_session, true)
|
|
465
479
|
tab.shell = nil
|
|
480
|
+
tab.shell_agent = nil
|
|
466
481
|
tab.clear_transient_shell_entries
|
|
467
482
|
tab.stream_state = new_tab_stream_state(tab.driver)
|
|
468
483
|
tab.markdown_chunks.clear
|
|
@@ -495,6 +510,7 @@ module Kward
|
|
|
495
510
|
end
|
|
496
511
|
|
|
497
512
|
def activate_tab(index, render: true)
|
|
513
|
+
service_background_tab_runs
|
|
498
514
|
tab = @tabs[index]
|
|
499
515
|
return nil unless tab
|
|
500
516
|
|
|
@@ -511,6 +527,7 @@ module Kward
|
|
|
511
527
|
@prompt.update_assistant_label(assistant_prompt_name) if @prompt.respond_to?(:update_assistant_label)
|
|
512
528
|
end
|
|
513
529
|
tab.unread = false
|
|
530
|
+
tab.attention = nil
|
|
514
531
|
restore_tab_composer_snapshot(tab.snapshot)
|
|
515
532
|
update_prompt_workspace_root(current_workspace_root)
|
|
516
533
|
@prompt.refresh_composer_status if @prompt.respond_to?(:refresh_composer_status)
|
|
@@ -597,6 +614,44 @@ module Kward
|
|
|
597
614
|
index ? index + 1 : active_tab_number
|
|
598
615
|
end
|
|
599
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
|
+
|
|
600
655
|
def submit_tab_input(tab, input, display_input: nil)
|
|
601
656
|
return if input.to_s.strip.empty?
|
|
602
657
|
|
|
@@ -610,6 +665,7 @@ module Kward
|
|
|
610
665
|
print_user_transcript(input, display_input: display_input) if prompt_interface?
|
|
611
666
|
tab.status = "queued"
|
|
612
667
|
tab.unread = false
|
|
668
|
+
tab.attention = nil
|
|
613
669
|
tab.cancellation = Cancellation.new
|
|
614
670
|
tab.steering = tab.driver.supports_steering? && steering_supported? ? Steering.new : nil
|
|
615
671
|
tab.error = nil
|
|
@@ -633,14 +689,17 @@ module Kward
|
|
|
633
689
|
end
|
|
634
690
|
tab.status = "ready"
|
|
635
691
|
tab.unread = tab != active_tab
|
|
692
|
+
tab.attention = :output if tab != active_tab
|
|
636
693
|
rescue Cancellation::CancelledError
|
|
637
694
|
tab.status = "cancelled"
|
|
638
695
|
tab.unread = false
|
|
696
|
+
tab.attention = :failure if tab != active_tab
|
|
639
697
|
report_tab_runtime_error(tab)
|
|
640
698
|
rescue StandardError => e
|
|
641
699
|
tab.error = e.message
|
|
642
700
|
tab.status = "failed"
|
|
643
701
|
tab.unread = false
|
|
702
|
+
tab.attention = :failure if tab != active_tab
|
|
644
703
|
report_tab_runtime_error(tab)
|
|
645
704
|
ensure
|
|
646
705
|
finish_tab_turn(tab)
|
|
@@ -656,8 +715,9 @@ module Kward
|
|
|
656
715
|
end
|
|
657
716
|
|
|
658
717
|
def poll_active_tab_input
|
|
718
|
+
service_background_tab_runs
|
|
659
719
|
tab = active_tab
|
|
660
|
-
unless tab&.running?
|
|
720
|
+
unless tab&.running? || tab&.local_busy?
|
|
661
721
|
input = @prompt.ask("You>")
|
|
662
722
|
return { tab_action: :close } if input.nil? && tab && @tabs.length > 1
|
|
663
723
|
|
|
@@ -666,6 +726,7 @@ module Kward
|
|
|
666
726
|
|
|
667
727
|
@prompt.begin_busy_input("You>") if @prompt.respond_to?(:begin_busy_input)
|
|
668
728
|
loop do
|
|
729
|
+
service_background_tab_runs
|
|
669
730
|
refresh_active_tab
|
|
670
731
|
if service_active_tab_question
|
|
671
732
|
return next_tab_queued_input(tab) if tab.idle? && !tab.queued_inputs.empty?
|
|
@@ -673,6 +734,10 @@ module Kward
|
|
|
673
734
|
|
|
674
735
|
next
|
|
675
736
|
end
|
|
737
|
+
if @prompt.respond_to?(:modal_active?) && @prompt.modal_active?
|
|
738
|
+
sleep 0.01
|
|
739
|
+
next
|
|
740
|
+
end
|
|
676
741
|
poll_result = @prompt.poll_input
|
|
677
742
|
case poll_result
|
|
678
743
|
when Hash
|
|
@@ -681,8 +746,12 @@ module Kward
|
|
|
681
746
|
return poll_result
|
|
682
747
|
end
|
|
683
748
|
when PromptInterface::CANCEL_INPUT
|
|
684
|
-
tab.
|
|
685
|
-
|
|
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
|
|
686
755
|
when PromptInterface::EXIT_INPUT
|
|
687
756
|
tab.queued_inputs << "/exit"
|
|
688
757
|
@prompt.set_queued_count(tab.queued_inputs.length) if @prompt.respond_to?(:set_queued_count)
|
|
@@ -824,15 +893,17 @@ module Kward
|
|
|
824
893
|
return if tab.stream_state[:streamed]
|
|
825
894
|
return if tab.answer.to_s.empty?
|
|
826
895
|
|
|
827
|
-
@prompt.say("\n#{colored(
|
|
896
|
+
@prompt.say("\n#{colored(transcript_marker("Assistant"), :activity, :bold)} #{render_markdown_transcript(tab.answer)}\n")
|
|
828
897
|
end
|
|
829
898
|
|
|
830
899
|
def new_tab_stream_state(driver)
|
|
831
900
|
{
|
|
832
901
|
streamed: false,
|
|
902
|
+
assistant_arrived: false,
|
|
833
903
|
last_flush: monotonic_now,
|
|
834
904
|
stream_block_open: false,
|
|
835
905
|
markdown_streams: {},
|
|
906
|
+
tool_elapsed_ms: {},
|
|
836
907
|
defer_assistant_streaming: driver.respond_to?(:conversation) && defer_assistant_streaming?(driver.agent)
|
|
837
908
|
}
|
|
838
909
|
end
|
|
@@ -847,10 +918,11 @@ module Kward
|
|
|
847
918
|
end
|
|
848
919
|
|
|
849
920
|
def tab_label_color(tab)
|
|
850
|
-
return :
|
|
851
|
-
return :
|
|
852
|
-
return :
|
|
853
|
-
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
|
|
854
926
|
|
|
855
927
|
nil
|
|
856
928
|
end
|
|
@@ -959,11 +1031,17 @@ module Kward
|
|
|
959
1031
|
def stop_tabs
|
|
960
1032
|
stop_tab_live_view
|
|
961
1033
|
Array(@tabs).each do |tab|
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
1034
|
+
if tab&.running?
|
|
1035
|
+
tab.cancellation&.cancel!
|
|
1036
|
+
tab.thread&.raise(Cancellation::CancelledError, "cancelled") if tab.thread&.alive?
|
|
1037
|
+
tab.thread&.join(0.2)
|
|
1038
|
+
end
|
|
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)
|
|
967
1045
|
rescue StandardError
|
|
968
1046
|
nil
|
|
969
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
|
@@ -10,11 +10,16 @@ require_relative "compactor"
|
|
|
10
10
|
require_relative "config_files"
|
|
11
11
|
require_relative "clipboard"
|
|
12
12
|
require_relative "cancellation"
|
|
13
|
+
require_relative "editor_prompt"
|
|
14
|
+
require_relative "editor_prompt_session"
|
|
15
|
+
require_relative "shell_prompt"
|
|
16
|
+
require_relative "shell_prompt_session"
|
|
13
17
|
require_relative "cli_transcript_formatter"
|
|
14
18
|
require_relative "model/context_usage"
|
|
15
19
|
require_relative "events"
|
|
16
20
|
require_relative "export_path"
|
|
17
|
-
require_relative "
|
|
21
|
+
require_relative "kwsh"
|
|
22
|
+
require_relative "persistent_shell_session"
|
|
18
23
|
require_relative "interactive_pty_runner"
|
|
19
24
|
require_relative "adaptive_pty_output_sink"
|
|
20
25
|
require_relative "pty_transcript_normalizer"
|
|
@@ -87,7 +92,7 @@ module Kward
|
|
|
87
92
|
RESTORED_TOOL_OUTPUT_LIMIT = 2_000
|
|
88
93
|
INTERACTIVE_TOOL_OUTPUT_LINE_LIMIT = 10
|
|
89
94
|
STREAM_RENDER_INTERVAL = 0.025
|
|
90
|
-
INTERACTIVE_EVENT_DRAIN_LIMIT =
|
|
95
|
+
INTERACTIVE_EVENT_DRAIN_LIMIT = 12
|
|
91
96
|
BUILTIN_SLASH_COMMANDS = PromptCommands::BUILTIN_COMMANDS
|
|
92
97
|
BUILTIN_SLASH_COMMAND_NAMES = PromptCommands::BUILTIN_RESERVED_COMMAND_NAMES
|
|
93
98
|
|
|
@@ -151,6 +156,16 @@ module Kward
|
|
|
151
156
|
warn e.message
|
|
152
157
|
warn "Run `kward help` for available commands."
|
|
153
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
|
|
154
169
|
ensure
|
|
155
170
|
ConfigFiles.skip_config = false
|
|
156
171
|
end
|
|
@@ -179,6 +194,30 @@ module Kward
|
|
|
179
194
|
MESSAGE
|
|
180
195
|
end
|
|
181
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
|
+
|
|
182
221
|
def dispatch
|
|
183
222
|
if @prompt_delimited
|
|
184
223
|
ConfigFiles.ensure_default_config!
|
|
@@ -224,7 +263,7 @@ module Kward
|
|
|
224
263
|
raise ArgumentError, command_usage("doctor") unless @argv.length == 1
|
|
225
264
|
|
|
226
265
|
ensure_client!
|
|
227
|
-
print_doctor
|
|
266
|
+
exit 1 unless print_doctor
|
|
228
267
|
return
|
|
229
268
|
end
|
|
230
269
|
|
|
@@ -234,7 +273,7 @@ module Kward
|
|
|
234
273
|
return
|
|
235
274
|
end
|
|
236
275
|
|
|
237
|
-
handle_hooks_command((@argv[1..] || []).join(" "))
|
|
276
|
+
handle_hooks_command((@argv[1..] || []).join(" "), command_line: true)
|
|
238
277
|
return
|
|
239
278
|
end
|
|
240
279
|
|
|
@@ -473,10 +512,16 @@ module Kward
|
|
|
473
512
|
install_bang_completion_provider(agent)
|
|
474
513
|
|
|
475
514
|
loop do
|
|
476
|
-
if
|
|
477
|
-
|
|
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))
|
|
478
518
|
end
|
|
479
519
|
input = @pending_inputs.shift || (active_tab ? poll_active_tab_input : @prompt.ask("You>"))
|
|
520
|
+
if input.is_a?(Hash) && input[:editor_prompt]
|
|
521
|
+
pending_inputs = run_editor_prompt_turn(input[:editor_prompt], active_tab&.agent || agent)
|
|
522
|
+
pending_inputs.reverse_each { |pending_input| @pending_inputs.unshift(pending_input) }
|
|
523
|
+
next
|
|
524
|
+
end
|
|
480
525
|
if input.is_a?(Hash) && input[:tab_action]
|
|
481
526
|
tab_result = handle_tab_action(input, session_store)
|
|
482
527
|
break if tab_result == PromptInterface::EXIT_INPUT
|
|
@@ -527,7 +572,10 @@ module Kward
|
|
|
527
572
|
end
|
|
528
573
|
end
|
|
529
574
|
next if handled
|
|
530
|
-
|
|
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
|
|
531
579
|
|
|
532
580
|
flush_pending_reasoning_config(conversation: active_tab.agent.conversation) if active_tab&.agent
|
|
533
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
|
|
|
@@ -31,13 +31,15 @@ module Kward
|
|
|
31
31
|
parts = [role]
|
|
32
32
|
if role.to_s == "compactionSummary"
|
|
33
33
|
parts << value(message, :summary)
|
|
34
|
-
|
|
34
|
+
elsif response_items(message).empty?
|
|
35
35
|
parts << content_text(value(message, :content))
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
parts << value(message, :reasoning_summary)
|
|
37
|
+
tool_calls(message).each do |tool_call|
|
|
38
|
+
parts << tool_call_name(tool_call)
|
|
39
|
+
parts << tool_call_arguments(tool_call)
|
|
40
|
+
end
|
|
41
|
+
else
|
|
42
|
+
parts << JSON.generate(response_items(message))
|
|
41
43
|
end
|
|
42
44
|
parts << value(message, :tool_call_id)
|
|
43
45
|
parts << value(message, :name)
|
|
@@ -65,6 +67,10 @@ module Kward
|
|
|
65
67
|
MessageAccess.tool_calls(message)
|
|
66
68
|
end
|
|
67
69
|
|
|
70
|
+
def response_items(message)
|
|
71
|
+
MessageAccess.response_items(message)
|
|
72
|
+
end
|
|
73
|
+
|
|
68
74
|
def tool_call_name(tool_call)
|
|
69
75
|
ToolCall.name(tool_call)
|
|
70
76
|
end
|
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)
|
|
@@ -136,6 +141,8 @@ module Kward
|
|
|
136
141
|
},
|
|
137
142
|
"editor" => {
|
|
138
143
|
"mode" => "modern",
|
|
144
|
+
"agent" => {},
|
|
145
|
+
"runners" => {},
|
|
139
146
|
"auto_indent" => true,
|
|
140
147
|
"auto_close_pairs" => true,
|
|
141
148
|
"soft_wrap" => true,
|
|
@@ -339,67 +346,25 @@ module Kward
|
|
|
339
346
|
merged
|
|
340
347
|
end
|
|
341
348
|
|
|
342
|
-
def
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
end
|
|
351
|
-
|
|
352
|
-
def normalize_ekwsh_config(data)
|
|
353
|
-
data = data.transform_keys(&:to_s) if data.is_a?(Hash)
|
|
354
|
-
settings = data.is_a?(Hash) ? data : {}
|
|
355
|
-
{
|
|
356
|
-
shell: normalize_ekwsh_shell(settings["shell"]),
|
|
357
|
-
timeout_seconds: normalize_positive_integer(settings["timeout_seconds"], Ekwsh::DEFAULT_TIMEOUT_SECONDS),
|
|
358
|
-
max_output_bytes: normalize_positive_integer(settings["max_output_bytes"], Ekwsh::DEFAULT_MAX_OUTPUT_BYTES),
|
|
359
|
-
history_limit: normalize_positive_integer(settings["history_limit"], Ekwsh::DEFAULT_HISTORY_LIMIT),
|
|
360
|
-
env: normalize_ekwsh_env(settings["env"]),
|
|
361
|
-
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: {}
|
|
362
357
|
}
|
|
358
|
+
rc_config = read_kwshrc_config
|
|
359
|
+
config[:env].merge!(rc_config[:env])
|
|
360
|
+
config[:aliases].merge!(rc_config[:aliases])
|
|
361
|
+
config
|
|
363
362
|
end
|
|
364
363
|
|
|
365
|
-
def
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
Ekwsh::DEFAULT_SHELL
|
|
371
|
-
end
|
|
372
|
-
|
|
373
|
-
def normalize_positive_integer(value, default)
|
|
374
|
-
integer = Integer(value)
|
|
375
|
-
integer.positive? ? integer : default
|
|
376
|
-
rescue ArgumentError, TypeError
|
|
377
|
-
default
|
|
378
|
-
end
|
|
379
|
-
|
|
380
|
-
def normalize_ekwsh_env(values)
|
|
381
|
-
return {} unless values.is_a?(Hash)
|
|
382
|
-
|
|
383
|
-
values.each_with_object({}) do |(key, value), result|
|
|
384
|
-
key = key.to_s
|
|
385
|
-
next unless key.match?(/\A[A-Za-z_][A-Za-z0-9_]*\z/)
|
|
386
|
-
next if value.nil?
|
|
387
|
-
|
|
388
|
-
result[key] = value.to_s
|
|
389
|
-
end
|
|
390
|
-
end
|
|
391
|
-
|
|
392
|
-
def normalize_ekwsh_aliases(values)
|
|
393
|
-
return {} unless values.is_a?(Hash)
|
|
394
|
-
|
|
395
|
-
values.each_with_object({}) do |(name, command), result|
|
|
396
|
-
name = name.to_s
|
|
397
|
-
command = command.to_s.strip
|
|
398
|
-
next unless Ekwsh.valid_alias_name?(name)
|
|
399
|
-
next if command.empty?
|
|
400
|
-
|
|
401
|
-
result[name] = command
|
|
402
|
-
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}"
|
|
403
368
|
end
|
|
404
369
|
|
|
405
370
|
# Merges top-level config values and writes the updated config privately.
|
|
@@ -482,6 +447,72 @@ module Kward
|
|
|
482
447
|
EditorMode.normalize(editor["mode"])
|
|
483
448
|
end
|
|
484
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
|
+
|
|
468
|
+
# Returns the optional model override for editor-agent turns.
|
|
469
|
+
def editor_agent_model(config = read_config)
|
|
470
|
+
editor_agent_setting(config, "model")
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
# Returns the optional reasoning-effort override for editor-agent turns.
|
|
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)
|
|
503
|
+
editor = config["editor"].is_a?(Hash) ? config["editor"] : {}
|
|
504
|
+
agent = editor["agent"].is_a?(Hash) ? editor["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
|
|
513
|
+
value.empty? ? nil : value
|
|
514
|
+
end
|
|
515
|
+
|
|
485
516
|
# Returns whether the built-in TUI editor should auto-indent new lines.
|
|
486
517
|
def editor_auto_indent?(config = read_config)
|
|
487
518
|
editor = config["editor"].is_a?(Hash) ? config["editor"] : {}
|
|
@@ -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
|