kward 0.80.0 → 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 +40 -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 +19 -5
- data/doc/tabs.md +1 -1
- data/doc/usage.md +6 -5
- data/lib/kward/agent.rb +11 -6
- 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/memory_commands.rb +2 -2
- 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 +54 -4
- data/lib/kward/cli/rendering.rb +28 -2
- data/lib/kward/cli/runtime_helpers.rb +157 -19
- data/lib/kward/cli/sessions.rb +6 -4
- data/lib/kward/cli/settings.rb +1 -1
- data/lib/kward/cli/slash_commands.rb +7 -1
- data/lib/kward/cli/tabs.rb +4 -1
- data/lib/kward/cli.rb +21 -1
- data/lib/kward/compactor.rb +7 -2
- data/lib/kward/config_files.rb +30 -9
- data/lib/kward/conversation.rb +8 -5
- data/lib/kward/ekwsh.rb +37 -16
- data/lib/kward/hooks/audit_log.rb +5 -2
- data/lib/kward/interactive_pty_runner.rb +88 -22
- data/lib/kward/plugin_registry.rb +20 -13
- 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/editor/syntax_highlighter.rb +35 -10
- 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/templates.rb +11 -6
- 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 +67 -12
- data/lib/kward/skills/trust_coordinator.rb +45 -0
- data/lib/kward/skills/trust_store.rb +107 -0
- data/lib/kward/telemetry/logger.rb +5 -2
- data/lib/kward/version.rb +1 -1
- metadata +5 -1
|
@@ -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
|
|
@@ -11,12 +11,24 @@ module Kward
|
|
|
11
11
|
return selection_overlay_rows(width, height: height) if @select_state
|
|
12
12
|
return git_overlay_rows(width, height: height) if @git_state
|
|
13
13
|
return project_browser_rows(width, height: height) if project_browser_visible?
|
|
14
|
+
return completion_overlay_rows(width, height: height) if @completion_overlay
|
|
14
15
|
return history_search_overlay_rows(width, height: height) if history_search_active?
|
|
15
16
|
return file_overlay_rows(width, height: height) if file_overlay_visible?
|
|
16
17
|
|
|
17
18
|
slash_overlay_rows(width, height: height)
|
|
18
19
|
end
|
|
19
20
|
|
|
21
|
+
def completion_overlay_rows(width, height: screen_height)
|
|
22
|
+
candidates = @completion_overlay[:candidates]
|
|
23
|
+
max_rows = max_overlay_list_rows(height)
|
|
24
|
+
selected = @completion_overlay[:index].to_i
|
|
25
|
+
start = centered_list_window_start(selected, candidates.length, max_rows)
|
|
26
|
+
rows = (candidates[start, max_rows] || []).each_with_index.map do |candidate, offset|
|
|
27
|
+
overlay_choice_line(candidate, selected: start + offset == selected)
|
|
28
|
+
end
|
|
29
|
+
overlay_card_rows("Completions", rows, width)
|
|
30
|
+
end
|
|
31
|
+
|
|
20
32
|
def history_search_overlay_rows(width, height: screen_height)
|
|
21
33
|
matches = history_search_matches
|
|
22
34
|
if matches.empty?
|
|
@@ -87,6 +87,11 @@ module Kward
|
|
|
87
87
|
ensure_scroll_region_locked(rows.length, width: width, height: height)
|
|
88
88
|
end
|
|
89
89
|
|
|
90
|
+
def make_room_for_composer_after_handoff_locked
|
|
91
|
+
rows, = composer_layout(screen_width, screen_height)
|
|
92
|
+
@output_io.print("\r\n" * rows.length)
|
|
93
|
+
end
|
|
94
|
+
|
|
90
95
|
def ensure_scroll_region_locked(row_count, redraw_transcript: true, width: screen_width, height: screen_height)
|
|
91
96
|
new_reserved_rows = [[row_count, 1].max, [height - 1, 1].max].min
|
|
92
97
|
return if @reserved_rows == new_reserved_rows && @last_height == height
|
|
@@ -128,7 +128,7 @@ module Kward
|
|
|
128
128
|
end
|
|
129
129
|
end
|
|
130
130
|
|
|
131
|
-
def initialize(input: $stdin, output: $stdout, slash_commands: [], overlay_settings: nil, project_browser_icon_theme: "off", footer: nil, composer_status: nil, busy_help: true, attachment_badges: nil, attachment_parser: nil, banner_message: nil, tab_keybindings: nil, prompt_history: nil, workspace_root: Dir.pwd, editor_mode: nil, editor_mode_source: nil, editor_auto_indent: true, editor_auto_indent_source: nil, editor_auto_close_pairs: true, editor_auto_close_pairs_source: nil, editor_soft_wrap: true, editor_soft_wrap_source: nil, editor_bar_cursor: true, editor_bar_cursor_source: nil, editor_line_numbers: "absolute", editor_line_numbers_source: nil, diff_view: "auto", diff_view_source: nil)
|
|
131
|
+
def initialize(input: $stdin, output: $stdout, slash_commands: [], overlay_settings: nil, project_browser_icon_theme: "off", footer: nil, composer_status: nil, busy_help: true, attachment_badges: nil, attachment_parser: nil, banner_message: nil, tab_keybindings: nil, prompt_history: nil, workspace_root: Dir.pwd, editor_mode: nil, editor_mode_source: nil, editor_auto_indent: true, editor_auto_indent_source: nil, editor_auto_close_pairs: true, editor_auto_close_pairs_source: nil, editor_soft_wrap: true, editor_soft_wrap_source: nil, editor_bar_cursor: true, editor_bar_cursor_source: nil, editor_line_numbers: "absolute", editor_line_numbers_source: nil, diff_view: "auto", diff_view_source: nil, redraw_handler: nil)
|
|
132
132
|
@input_io = input
|
|
133
133
|
@output_io = output
|
|
134
134
|
@reader = TTY::Reader.new(input: input, output: output, interrupt: :error)
|
|
@@ -162,6 +162,10 @@ module Kward
|
|
|
162
162
|
@restoring_transcript = false
|
|
163
163
|
@pending_keys = []
|
|
164
164
|
@completion_provider = nil
|
|
165
|
+
@completion_cycle = nil
|
|
166
|
+
@completion_overlay = nil
|
|
167
|
+
@redraw_handler = redraw_handler
|
|
168
|
+
@transcript_redraw_requested = false
|
|
165
169
|
@original_console_mode = nil
|
|
166
170
|
@raw_mode_active = false
|
|
167
171
|
@slash_commands = normalize_slash_commands(slash_commands)
|
|
@@ -258,24 +262,11 @@ module Kward
|
|
|
258
262
|
end
|
|
259
263
|
|
|
260
264
|
def say(message)
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
if @restoring_transcript
|
|
264
|
-
write_transcript_text_locked(text)
|
|
265
|
-
write_transcript_text_locked("\n") unless text.end_with?("\n")
|
|
266
|
-
@stream_state.finish_block
|
|
267
|
-
next
|
|
268
|
-
end
|
|
265
|
+
write_transcript_message(message, preserve_composer: false)
|
|
266
|
+
end
|
|
269
267
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
write_transcript_text_locked(text)
|
|
273
|
-
write_transcript_text_locked("\n") unless text.end_with?("\n")
|
|
274
|
-
@stream_state.finish_block
|
|
275
|
-
render_prompt_after_output_locked
|
|
276
|
-
end
|
|
277
|
-
@output_io.flush
|
|
278
|
-
end
|
|
268
|
+
def write_transcript(message)
|
|
269
|
+
write_transcript_message(message, preserve_composer: true)
|
|
279
270
|
end
|
|
280
271
|
|
|
281
272
|
def say_visual(message)
|
|
@@ -328,6 +319,16 @@ module Kward
|
|
|
328
319
|
@slash_overlay_disabled = previous_slash_overlay_disabled
|
|
329
320
|
end
|
|
330
321
|
|
|
322
|
+
def update_completion_provider(provider, slash_overlay: true)
|
|
323
|
+
@mutex.synchronize do
|
|
324
|
+
@completion_provider = provider
|
|
325
|
+
@completion_cycle = nil
|
|
326
|
+
@completion_overlay = nil
|
|
327
|
+
@slash_overlay_disabled = !slash_overlay
|
|
328
|
+
render_prompt_locked if @started
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
|
|
331
332
|
def with_prompt_history(history)
|
|
332
333
|
previous_history = @prompt_history
|
|
333
334
|
@prompt_history = history
|
|
@@ -435,6 +436,8 @@ module Kward
|
|
|
435
436
|
reset_history_navigation
|
|
436
437
|
end
|
|
437
438
|
@pending_keys.clear
|
|
439
|
+
@completion_cycle = nil
|
|
440
|
+
@completion_overlay = nil
|
|
438
441
|
@asking = true
|
|
439
442
|
@busy = false
|
|
440
443
|
@queued_count = 0
|
|
@@ -454,11 +457,14 @@ module Kward
|
|
|
454
457
|
render_prompt_locked unless result.is_a?(String) || result == EXIT_INPUT || prompt_action_result?(result)
|
|
455
458
|
end
|
|
456
459
|
end
|
|
460
|
+
perform_pending_transcript_redraw
|
|
457
461
|
return result if result.is_a?(String) || prompt_action_result?(result)
|
|
458
462
|
return nil if result == EXIT_INPUT
|
|
459
463
|
|
|
460
464
|
sleep 0.02 if key.nil?
|
|
461
465
|
end
|
|
466
|
+
ensure
|
|
467
|
+
perform_pending_transcript_redraw
|
|
462
468
|
end
|
|
463
469
|
|
|
464
470
|
def yes?(message, default: false)
|
|
@@ -573,13 +579,19 @@ module Kward
|
|
|
573
579
|
end
|
|
574
580
|
end
|
|
575
581
|
|
|
576
|
-
def with_terminal_handoff
|
|
582
|
+
def with_terminal_handoff(preserve_composer: false)
|
|
577
583
|
start
|
|
578
584
|
input = nil
|
|
579
585
|
output = nil
|
|
580
586
|
@mutex.synchronize do
|
|
581
|
-
|
|
582
|
-
|
|
587
|
+
if preserve_composer
|
|
588
|
+
handle_resize_locked
|
|
589
|
+
reserve_composer_region_locked
|
|
590
|
+
move_to_transcript_cursor_locked
|
|
591
|
+
else
|
|
592
|
+
clear_prompt_for_output_locked
|
|
593
|
+
restore_scroll_region_locked
|
|
594
|
+
end
|
|
583
595
|
disable_editor_mouse_reporting(force: true)
|
|
584
596
|
@output_io.print(BRACKETED_PASTE_RESTORE)
|
|
585
597
|
@output_io.print(KEYBOARD_PROTOCOL_RESTORE)
|
|
@@ -594,6 +606,8 @@ module Kward
|
|
|
594
606
|
yield(input, output)
|
|
595
607
|
ensure
|
|
596
608
|
@mutex.synchronize do
|
|
609
|
+
restore_scroll_region_locked if preserve_composer
|
|
610
|
+
make_room_for_composer_after_handoff_locked if !preserve_composer && @started && @asking
|
|
597
611
|
enter_raw_mode_locked
|
|
598
612
|
@output_io.print(KEYBOARD_PROTOCOL_ENABLE)
|
|
599
613
|
@output_io.print(BRACKETED_PASTE_ENABLE)
|
|
@@ -683,6 +697,8 @@ module Kward
|
|
|
683
697
|
|
|
684
698
|
def restore_composer_snapshot_locked(snapshot)
|
|
685
699
|
@composer = snapshot[:composer] || new_composer_state_with_history
|
|
700
|
+
@completion_cycle = nil
|
|
701
|
+
@completion_overlay = nil
|
|
686
702
|
@prompt_label = snapshot[:prompt_label].to_s.empty? ? "You>" : snapshot[:prompt_label].to_s
|
|
687
703
|
self.composer_input = @composer.input
|
|
688
704
|
self.composer_cursor = @composer.cursor
|
|
@@ -811,6 +827,8 @@ module Kward
|
|
|
811
827
|
render_prompt_locked unless [EXIT_INPUT, CANCEL_INPUT].include?(result) || prompt_action_result?(result)
|
|
812
828
|
[EXIT_INPUT, CANCEL_INPUT].include?(result) ? result : result
|
|
813
829
|
end
|
|
830
|
+
ensure
|
|
831
|
+
perform_pending_transcript_redraw
|
|
814
832
|
end
|
|
815
833
|
|
|
816
834
|
def update_assistant_label(label)
|
|
@@ -855,6 +873,21 @@ module Kward
|
|
|
855
873
|
end
|
|
856
874
|
end
|
|
857
875
|
|
|
876
|
+
def record_transient_terminal_output(text)
|
|
877
|
+
value = text.to_s
|
|
878
|
+
return if value.empty?
|
|
879
|
+
|
|
880
|
+
@mutex.synchronize do
|
|
881
|
+
append_transcript_buffer(value)
|
|
882
|
+
append_transcript_buffer("\n") unless value.end_with?("\n")
|
|
883
|
+
@stream_state.finish_block
|
|
884
|
+
width, height = screen_size
|
|
885
|
+
remember_transcript_viewport_locked(height)
|
|
886
|
+
with_synchronized_output_locked { redraw_screen_locked(width: width, height: height) }
|
|
887
|
+
@output_io.flush
|
|
888
|
+
end
|
|
889
|
+
end
|
|
890
|
+
|
|
858
891
|
def write_transcript_delta(delta)
|
|
859
892
|
@mutex.synchronize do
|
|
860
893
|
with_synchronized_output_locked do
|
|
@@ -904,6 +937,45 @@ module Kward
|
|
|
904
937
|
|
|
905
938
|
private
|
|
906
939
|
|
|
940
|
+
def write_transcript_message(message, preserve_composer:)
|
|
941
|
+
@mutex.synchronize do
|
|
942
|
+
text = message.to_s
|
|
943
|
+
if @restoring_transcript
|
|
944
|
+
write_transcript_text_locked(text)
|
|
945
|
+
write_transcript_text_locked("\n") unless text.end_with?("\n")
|
|
946
|
+
@stream_state.finish_block
|
|
947
|
+
next
|
|
948
|
+
end
|
|
949
|
+
|
|
950
|
+
with_synchronized_output_locked do
|
|
951
|
+
if preserve_composer
|
|
952
|
+
prepare_transcript_output_locked
|
|
953
|
+
else
|
|
954
|
+
clear_prompt_for_output_locked
|
|
955
|
+
end
|
|
956
|
+
write_transcript_text_locked(text)
|
|
957
|
+
write_transcript_text_locked("\n") unless text.end_with?("\n")
|
|
958
|
+
@stream_state.finish_block
|
|
959
|
+
preserve_composer ? restore_composer_cursor_locked : render_prompt_after_output_locked
|
|
960
|
+
end
|
|
961
|
+
@output_io.flush
|
|
962
|
+
end
|
|
963
|
+
end
|
|
964
|
+
|
|
965
|
+
def request_transcript_redraw
|
|
966
|
+
redraw_screen_locked
|
|
967
|
+
@transcript_redraw_requested = true
|
|
968
|
+
end
|
|
969
|
+
|
|
970
|
+
def perform_pending_transcript_redraw
|
|
971
|
+
requested = @mutex.synchronize do
|
|
972
|
+
pending = @transcript_redraw_requested
|
|
973
|
+
@transcript_redraw_requested = false
|
|
974
|
+
pending
|
|
975
|
+
end
|
|
976
|
+
@redraw_handler&.call if requested
|
|
977
|
+
end
|
|
978
|
+
|
|
907
979
|
def prompt_workspace_root
|
|
908
980
|
@workspace_root
|
|
909
981
|
end
|
|
@@ -28,6 +28,7 @@ module Kward
|
|
|
28
28
|
{ name: "diff", description: "Open the file changes recorded in this session.", argument_hint: "" },
|
|
29
29
|
{ name: "files", description: "Browse project files.", argument_hint: "" },
|
|
30
30
|
{ name: "shell", description: "Open the embedded Kward shell.", argument_hint: "" },
|
|
31
|
+
{ name: "capture", description: "Run a command with bounded transcript capture.", argument_hint: "<command>" },
|
|
31
32
|
{ name: "scratchpad", description: "Open an unsaved editor buffer.", argument_hint: "[text|markdown|ruby]" },
|
|
32
33
|
{ name: "pty", description: "Run a command in an interactive PTY passthrough session.", argument_hint: "<command>" },
|
|
33
34
|
{ name: "tab", description: "Manage tabs.", argument_hint: "[1-n|move|close|new|name|worktree]" },
|
|
@@ -36,6 +37,7 @@ module Kward
|
|
|
36
37
|
{ name: "sandbox", description: "Inspect or configure model command sandboxing.", argument_hint: "[status|off|read_only|workspace_write|network allow|network deny]" },
|
|
37
38
|
{ name: "hooks", description: "Inspect lifecycle hooks.", argument_hint: "[list|events|logs|doctor|trust|untrust]" },
|
|
38
39
|
{ name: "skill", description: "Activate a skill or capture one from a saved session.", argument_hint: "<name>|capture" },
|
|
40
|
+
{ name: "skills", description: "Review project skill trust.", argument_hint: "[status|trust|untrust]" },
|
|
39
41
|
{ name: "memory", description: "Inspect and manage Kward memory.", argument_hint: "[enable|disable|auto-summary|core|add|list|forget|promote|relax|inspect|why|summarize]" }
|
|
40
42
|
].freeze
|
|
41
43
|
BUILTIN_RESERVED_COMMAND_NAMES = BUILTIN_COMMANDS.map { |command| command[:name] }.freeze
|
|
@@ -4,10 +4,11 @@ module Kward
|
|
|
4
4
|
module Prompts
|
|
5
5
|
# Parsed prompt template loaded from disk.
|
|
6
6
|
class Templates
|
|
7
|
-
def initialize(config_dir:, template_class:, markdown_parser:)
|
|
7
|
+
def initialize(config_dir:, template_class:, markdown_parser:, warning_sink: nil)
|
|
8
8
|
@config_dir = config_dir
|
|
9
9
|
@template_class = template_class
|
|
10
10
|
@markdown_parser = markdown_parser
|
|
11
|
+
@warning_sink = warning_sink
|
|
11
12
|
end
|
|
12
13
|
|
|
13
14
|
def prompt_templates(reserved_commands: [])
|
|
@@ -21,11 +22,11 @@ module Kward
|
|
|
21
22
|
next unless template
|
|
22
23
|
|
|
23
24
|
if reserved.include?(template.command)
|
|
24
|
-
|
|
25
|
+
emit_warning "Warning: skipping Kward prompt command /#{template.command}: reserved command"
|
|
25
26
|
next
|
|
26
27
|
end
|
|
27
28
|
if seen[template.command]
|
|
28
|
-
|
|
29
|
+
emit_warning "Warning: skipping duplicate Kward prompt command /#{template.command}: #{path}"
|
|
29
30
|
next
|
|
30
31
|
end
|
|
31
32
|
|
|
@@ -33,7 +34,7 @@ module Kward
|
|
|
33
34
|
template
|
|
34
35
|
end
|
|
35
36
|
rescue StandardError => e
|
|
36
|
-
|
|
37
|
+
emit_warning "Warning: skipping Kward prompt templates in #{prompts_root}: #{e.message}"
|
|
37
38
|
[]
|
|
38
39
|
end
|
|
39
40
|
|
|
@@ -42,7 +43,7 @@ module Kward
|
|
|
42
43
|
def parse_prompt_template(path)
|
|
43
44
|
command = File.basename(path, ".md")
|
|
44
45
|
unless command.match?(/\A[A-Za-z0-9][A-Za-z0-9_-]*\z/)
|
|
45
|
-
|
|
46
|
+
emit_warning "Warning: skipping Kward prompt template #{path}: invalid command name"
|
|
46
47
|
return nil
|
|
47
48
|
end
|
|
48
49
|
|
|
@@ -55,9 +56,13 @@ module Kward
|
|
|
55
56
|
path: path
|
|
56
57
|
)
|
|
57
58
|
rescue StandardError => e
|
|
58
|
-
|
|
59
|
+
emit_warning "Warning: skipping Kward prompt template #{path}: #{e.message}"
|
|
59
60
|
nil
|
|
60
61
|
end
|
|
62
|
+
|
|
63
|
+
def emit_warning(message)
|
|
64
|
+
@warning_sink ? @warning_sink.call(message) : warn(message)
|
|
65
|
+
end
|
|
61
66
|
end
|
|
62
67
|
end
|
|
63
68
|
end
|
data/lib/kward/prompts.rb
CHANGED
|
@@ -22,22 +22,22 @@ module Kward
|
|
|
22
22
|
# @param plugin_context [String, nil] trusted plugin-provided instructions
|
|
23
23
|
# @return [Hash] role/content system message
|
|
24
24
|
# @api public
|
|
25
|
-
def system_message(workspace_root: Dir.pwd, include_workspace_personality: true, model: nil, reasoning_effort: nil, now: Time.now, memory_context: nil, plugin_context: nil, execution_profile_context: nil)
|
|
25
|
+
def system_message(workspace_root: Dir.pwd, include_workspace_personality: true, model: nil, reasoning_effort: nil, now: Time.now, memory_context: nil, plugin_context: nil, execution_profile_context: nil, project_skill_paths: nil)
|
|
26
26
|
{
|
|
27
27
|
role: "system",
|
|
28
|
-
content: prompt_parts(workspace_root: workspace_root, include_workspace_personality: include_workspace_personality, model: model, reasoning_effort: reasoning_effort, now: now, memory_context: memory_context, plugin_context: plugin_context, execution_profile_context: execution_profile_context).compact.join("\n\n")
|
|
28
|
+
content: prompt_parts(workspace_root: workspace_root, include_workspace_personality: include_workspace_personality, model: model, reasoning_effort: reasoning_effort, now: now, memory_context: memory_context, plugin_context: plugin_context, execution_profile_context: execution_profile_context, project_skill_paths: project_skill_paths).compact.join("\n\n")
|
|
29
29
|
}
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
-
def prompt_parts(workspace_root: Dir.pwd, include_workspace_personality: true, model: nil, reasoning_effort: nil, now: Time.now, memory_context: nil, plugin_context: nil, execution_profile_context: nil)
|
|
33
|
-
prompt_sections(workspace_root: workspace_root, include_workspace_personality: include_workspace_personality, model: model, reasoning_effort: reasoning_effort, now: now, memory_context: memory_context, plugin_context: plugin_context, execution_profile_context: execution_profile_context).map { |section| section[:content] }
|
|
32
|
+
def prompt_parts(workspace_root: Dir.pwd, include_workspace_personality: true, model: nil, reasoning_effort: nil, now: Time.now, memory_context: nil, plugin_context: nil, execution_profile_context: nil, project_skill_paths: nil)
|
|
33
|
+
prompt_sections(workspace_root: workspace_root, include_workspace_personality: include_workspace_personality, model: model, reasoning_effort: reasoning_effort, now: now, memory_context: memory_context, plugin_context: plugin_context, execution_profile_context: execution_profile_context, project_skill_paths: project_skill_paths).map { |section| section[:content] }
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
# Returns labeled prompt sections for inspection by CLI and RPC frontends.
|
|
37
37
|
#
|
|
38
38
|
# @return [Array<Hash>] section hashes with label, content, and optional source
|
|
39
39
|
# @api public
|
|
40
|
-
def prompt_sections(workspace_root: Dir.pwd, include_workspace_personality: true, model: nil, reasoning_effort: nil, now: Time.now, memory_context: nil, plugin_context: nil, execution_profile_context: nil)
|
|
40
|
+
def prompt_sections(workspace_root: Dir.pwd, include_workspace_personality: true, model: nil, reasoning_effort: nil, now: Time.now, memory_context: nil, plugin_context: nil, execution_profile_context: nil, project_skill_paths: nil)
|
|
41
41
|
return replacement_prompt_sections if ConfigFiles.replacement_system_prompt?
|
|
42
42
|
|
|
43
43
|
sections = [prompt_section("Built-in system prompt", base_prompt)]
|
|
@@ -48,7 +48,7 @@ module Kward
|
|
|
48
48
|
sections << prompt_section("Persona", persona_prompt(workspace_root, model: model, reasoning_effort: reasoning_effort, now: now))
|
|
49
49
|
sections << prompt_section("Plugin context", plugin_context) unless plugin_context.to_s.empty?
|
|
50
50
|
end
|
|
51
|
-
skills = ConfigFiles.skills(workspace_root: workspace_root)
|
|
51
|
+
skills = ConfigFiles.skills(workspace_root: workspace_root, project_skill_paths: project_skill_paths)
|
|
52
52
|
sections << prompt_section("Configured skills", skills_prompt(skills), source: skills.empty? ? nil : File.join(ConfigFiles.config_dir, "skills"))
|
|
53
53
|
sections << prompt_section(workspace_agents_context_label(workspace_root), workspace_agents_context(workspace_root), source: ConfigFiles.workspace_agents_file?(workspace_root) ? ConfigFiles.workspace_agents_path(workspace_root) : nil)
|
|
54
54
|
sections.compact
|
data/lib/kward/rpc/server.rb
CHANGED
|
@@ -579,6 +579,7 @@ module Kward
|
|
|
579
579
|
stability: { protocol: "stable", compatibility: "additive-fields-unless-protocol-version-changes", experimentalCapabilities: [] },
|
|
580
580
|
commands: { supported: true, methods: COMMAND_METHODS, method: COMMAND_METHODS[0], runMethod: COMMAND_METHODS[1], sources: ["builtin", "prompt", "skill", "plugin"], executableSources: ["builtin", "plugin"] },
|
|
581
581
|
skillCapture: { supported: true, methods: SKILL_CAPTURE_METHODS, destination: "personal", source: "savedSessionActiveLeaf", reviewRequired: true, overwrite: "explicit", autoActivate: false },
|
|
582
|
+
projectSkillTrust: { supported: false, trustRequired: true, reason: "RPC has no interactive trust decision bridge; project skills remain skipped unless globally enabled." },
|
|
582
583
|
mcp: {
|
|
583
584
|
supported: true,
|
|
584
585
|
transport: "stdio",
|
data/lib/kward/session_store.rb
CHANGED
|
@@ -297,7 +297,7 @@ module Kward
|
|
|
297
297
|
# `workspace` is used both for the active root and to restore read-before-write
|
|
298
298
|
# paths from successful read tool results. If a session moved workspaces, load
|
|
299
299
|
# it through `session_location` first so the original cwd is respected.
|
|
300
|
-
def load(path, workspace: Workspace.new, provider: nil, model: nil, reasoning_effort: nil)
|
|
300
|
+
def load(path, workspace: Workspace.new, provider: nil, model: nil, reasoning_effort: nil, project_skill_paths: nil)
|
|
301
301
|
resolved_path = resolve_session_path(path)
|
|
302
302
|
records = records_from_file(resolved_path)
|
|
303
303
|
header = session_header(records, resolved_path)
|
|
@@ -317,7 +317,8 @@ module Kward
|
|
|
317
317
|
model: runtime["model"] || model,
|
|
318
318
|
reasoning_effort: runtime["reasoningEffort"] || reasoning_effort,
|
|
319
319
|
session_memories: memory_state["sessionMemories"],
|
|
320
|
-
last_memory_retrieval: memory_state["lastRetrieval"]
|
|
320
|
+
last_memory_retrieval: memory_state["lastRetrieval"],
|
|
321
|
+
project_skill_paths: project_skill_paths
|
|
321
322
|
)
|
|
322
323
|
restore_tool_output_artifacts(records, conversation)
|
|
323
324
|
conversation.mark_last_entry_compaction! if latest_record_type(records) == "compaction"
|