kward 0.79.0 → 0.80.1
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/.github/workflows/ci.yml +6 -0
- data/CHANGELOG.md +65 -1
- data/Gemfile.lock +2 -2
- data/README.md +31 -38
- data/doc/agent-tools.md +9 -10
- data/doc/api.md +3 -3
- data/doc/authentication.md +79 -110
- data/doc/code-search.md +1 -1
- data/doc/composer.md +1 -1
- data/doc/configuration.md +69 -26
- data/doc/context-budgeting.md +6 -6
- data/doc/context-tools.md +2 -2
- data/doc/editor.md +7 -7
- data/doc/extensibility.md +7 -29
- data/doc/files.md +6 -6
- data/doc/getting-started.md +2 -2
- data/doc/git.md +8 -2
- data/doc/lifecycle-hooks.md +5 -4
- data/doc/local-models.md +2 -2
- data/doc/mcp.md +5 -5
- data/doc/permissions.md +13 -4
- data/doc/plugins.md +39 -4
- data/doc/prompt-templates.md +105 -0
- data/doc/providers.md +140 -0
- data/doc/releasing.md +10 -2
- data/doc/rpc.md +54 -31
- data/doc/sandboxing.md +22 -0
- data/doc/security.md +22 -7
- data/doc/session-management.md +2 -4
- data/doc/skills.md +2 -0
- data/doc/tabs.md +37 -0
- data/doc/telegram.md +138 -0
- data/doc/transports.md +209 -0
- data/doc/usage.md +11 -3
- data/doc/web-search.md +3 -3
- data/doc/workspace-tools.md +2 -2
- data/examples/plugins/stardate_footer.rb +10 -0
- data/examples/plugins/telegram/plugin.rb +39 -0
- data/examples/plugins/telegram/telegram_api.rb +136 -0
- data/examples/plugins/telegram/telegram_transport.rb +304 -0
- data/lib/kward/agent.rb +11 -6
- data/lib/kward/ansi.rb +1 -0
- data/lib/kward/auth/api_key_store.rb +99 -0
- data/lib/kward/cli/auth_commands.rb +72 -13
- data/lib/kward/cli/commands.rb +7 -0
- data/lib/kward/cli/git.rb +31 -8
- data/lib/kward/cli/memory_commands.rb +2 -2
- data/lib/kward/cli/plugins.rb +1 -0
- data/lib/kward/cli/prompt_interface.rb +44 -1
- data/lib/kward/cli/rendering.rb +6 -2
- data/lib/kward/cli/runtime_helpers.rb +5 -2
- data/lib/kward/cli/sessions.rb +1 -1
- data/lib/kward/cli/settings.rb +97 -33
- data/lib/kward/cli/slash_commands.rb +1 -1
- data/lib/kward/cli/tabs.rb +103 -37
- data/lib/kward/cli/transports.rb +67 -0
- data/lib/kward/cli/worktrees.rb +360 -0
- data/lib/kward/cli.rb +22 -0
- data/lib/kward/compactor.rb +7 -2
- data/lib/kward/config_files.rb +51 -12
- data/lib/kward/conversation.rb +13 -7
- data/lib/kward/git_worktree_manager.rb +291 -0
- data/lib/kward/hooks/audit_log.rb +5 -2
- data/lib/kward/model/azure_openai_config.rb +54 -0
- data/lib/kward/model/catalog.rb +113 -0
- data/lib/kward/model/client.rb +225 -20
- data/lib/kward/model/model_info.rb +49 -0
- data/lib/kward/model/payloads.rb +63 -4
- data/lib/kward/model/provider_catalog.rb +114 -0
- data/lib/kward/model/sources.rb +70 -0
- data/lib/kward/model/stream_parser.rb +83 -13
- data/lib/kward/permissions/policy.rb +18 -4
- data/lib/kward/plugin_chat_runtime.rb +374 -0
- data/lib/kward/plugin_registry.rb +99 -19
- data/lib/kward/private_file.rb +9 -3
- data/lib/kward/prompt_interface/approval_prompt.rb +2 -0
- data/lib/kward/prompt_interface/composer_renderer.rb +1 -5
- data/lib/kward/prompt_interface/editor/auto_indent.rb +46 -0
- data/lib/kward/prompt_interface/editor/buffer.rb +18 -4
- data/lib/kward/prompt_interface/editor/controller.rb +7 -7
- data/lib/kward/prompt_interface/editor/modes/vibe.rb +2 -2
- data/lib/kward/prompt_interface/editor/renderer.rb +8 -8
- data/lib/kward/prompt_interface/editor/state.rb +3 -9
- data/lib/kward/prompt_interface/editor/syntax_highlighter.rb +219 -19
- data/lib/kward/prompt_interface/file_overlay.rb +2 -2
- data/lib/kward/prompt_interface/git_prompt.rb +7 -3
- data/lib/kward/prompt_interface/interactive/controller.rb +4 -4
- data/lib/kward/prompt_interface/interactive/renderer.rb +4 -1
- data/lib/kward/prompt_interface/interactive/state.rb +2 -2
- data/lib/kward/prompt_interface/project_browser.rb +25 -6
- data/lib/kward/prompt_interface/runtime_state.rb +22 -6
- data/lib/kward/prompt_interface/screen.rb +3 -0
- data/lib/kward/prompt_interface/selection_prompt.rb +7 -1
- data/lib/kward/prompt_interface/transcript_buffer.rb +24 -2
- data/lib/kward/prompt_interface.rb +53 -12
- data/lib/kward/prompts/commands.rb +1 -1
- data/lib/kward/prompts/templates.rb +11 -6
- data/lib/kward/prompts.rb +6 -5
- data/lib/kward/rpc/auth_manager.rb +112 -152
- data/lib/kward/rpc/config_manager.rb +46 -6
- data/lib/kward/rpc/plugin_chat_manager.rb +55 -196
- data/lib/kward/rpc/prompt_bridge.rb +8 -4
- data/lib/kward/rpc/redactor.rb +1 -1
- data/lib/kward/rpc/server.rb +60 -10
- data/lib/kward/rpc/session_manager.rb +109 -30
- data/lib/kward/session_store.rb +84 -24
- data/lib/kward/skills/registry.rb +16 -11
- data/lib/kward/tab_driver.rb +6 -3
- data/lib/kward/telemetry/logger.rb +5 -2
- data/lib/kward/tools/git_commit.rb +39 -0
- data/lib/kward/tools/registry.rb +7 -2
- data/lib/kward/tools/tool_call.rb +1 -0
- data/lib/kward/transport/gateway.rb +253 -0
- data/lib/kward/transport/host.rb +343 -0
- data/lib/kward/transport/manager.rb +179 -0
- data/lib/kward/transport/plugin_chat_gateway.rb +186 -0
- data/lib/kward/transport/runtime.rb +41 -0
- data/lib/kward/transport/store.rb +101 -0
- data/lib/kward/transport.rb +204 -0
- data/lib/kward/version.rb +1 -1
- data/lib/kward/workspace_factory.rb +19 -2
- data/templates/default/fulldoc/html/js/kward.js +2 -0
- data/templates/default/kward_navigation.rb +7 -2
- data/templates/default/layout/html/setup.rb +4 -0
- metadata +26 -1
|
@@ -55,6 +55,9 @@ module Kward
|
|
|
55
55
|
controller = @interactive_state[:controller]
|
|
56
56
|
return false if controller.exited?
|
|
57
57
|
|
|
58
|
+
return true if controller.dirty?
|
|
59
|
+
return false unless controller.tickable?
|
|
60
|
+
|
|
58
61
|
now = monotonic_now
|
|
59
62
|
interval = 1.0 / controller.fps
|
|
60
63
|
elapsed = now - @last_interactive_tick
|
|
@@ -64,7 +67,7 @@ module Kward
|
|
|
64
67
|
@last_interactive_tick += steps * interval
|
|
65
68
|
result = controller.invoke_tick
|
|
66
69
|
controller.exit if result == :exit
|
|
67
|
-
|
|
70
|
+
controller.dirty?
|
|
68
71
|
end
|
|
69
72
|
end
|
|
70
73
|
end
|
|
@@ -125,6 +125,14 @@ module Kward
|
|
|
125
125
|
insert_selected_project_browser_mention
|
|
126
126
|
elsif text == "/" && !project_browser_search_active?
|
|
127
127
|
activate_project_browser_search
|
|
128
|
+
elsif text == "j" && !project_browser_search_active?
|
|
129
|
+
select_next_project_browser_row
|
|
130
|
+
elsif text == "k" && !project_browser_search_active?
|
|
131
|
+
select_previous_project_browser_row
|
|
132
|
+
elsif text == "h" && !project_browser_search_active?
|
|
133
|
+
collapse_selected_project_browser_row
|
|
134
|
+
elsif text == "l" && !project_browser_search_active?
|
|
135
|
+
expand_selected_project_browser_row
|
|
128
136
|
elsif project_browser_search_active?
|
|
129
137
|
project_browser_append_search(text)
|
|
130
138
|
else
|
|
@@ -148,6 +156,14 @@ module Kward
|
|
|
148
156
|
toggle_project_browser_search
|
|
149
157
|
when "/"
|
|
150
158
|
activate_project_browser_search
|
|
159
|
+
when "j"
|
|
160
|
+
project_browser_search_active? ? project_browser_append_search(key) : select_next_project_browser_row
|
|
161
|
+
when "k"
|
|
162
|
+
project_browser_search_active? ? project_browser_append_search(key) : select_previous_project_browser_row
|
|
163
|
+
when "h"
|
|
164
|
+
project_browser_search_active? ? project_browser_append_search(key) : collapse_selected_project_browser_row
|
|
165
|
+
when "l"
|
|
166
|
+
project_browser_search_active? ? project_browser_append_search(key) : expand_selected_project_browser_row
|
|
151
167
|
else
|
|
152
168
|
project_browser_append_search(key) if project_browser_search_active? && printable_key?(key)
|
|
153
169
|
end
|
|
@@ -319,7 +335,7 @@ module Kward
|
|
|
319
335
|
if rows.empty?
|
|
320
336
|
lines << overlay_text_line(project_browser_empty_message, :muted)
|
|
321
337
|
else
|
|
322
|
-
visible = visible_project_browser_rows(rows, height: height)
|
|
338
|
+
visible = visible_project_browser_rows(rows, height: height, width: width)
|
|
323
339
|
visible[:rows].each_with_index do |row, offset|
|
|
324
340
|
index = visible[:start] + offset
|
|
325
341
|
lines << overlay_choice_line(project_browser_row_text(row), selected: index == @project_browser_state[:selection_index])
|
|
@@ -476,7 +492,7 @@ module Kward
|
|
|
476
492
|
end
|
|
477
493
|
|
|
478
494
|
def project_browser_workspace_root
|
|
479
|
-
ConfigFiles.canonical_workspace_root(
|
|
495
|
+
ConfigFiles.canonical_workspace_root(prompt_workspace_root)
|
|
480
496
|
end
|
|
481
497
|
|
|
482
498
|
def restored_project_browser_expanded_paths(paths, saved_state)
|
|
@@ -536,14 +552,17 @@ module Kward
|
|
|
536
552
|
@project_browser_state[:selection_index] = [[@project_browser_state[:selection_index], 0].max, rows.length - 1].min unless rows.empty?
|
|
537
553
|
end
|
|
538
554
|
|
|
539
|
-
def visible_project_browser_rows(rows, height: screen_height)
|
|
540
|
-
max_rows = max_project_browser_rows(height)
|
|
555
|
+
def visible_project_browser_rows(rows, height: screen_height, width: screen_width)
|
|
556
|
+
max_rows = max_project_browser_rows(height, width: width)
|
|
541
557
|
start = centered_list_window_start(@project_browser_state[:selection_index], rows.length, max_rows)
|
|
542
558
|
{ start: start, rows: rows[start, max_rows] || [] }
|
|
543
559
|
end
|
|
544
560
|
|
|
545
|
-
def max_project_browser_rows(height)
|
|
546
|
-
|
|
561
|
+
def max_project_browser_rows(height, width: screen_width)
|
|
562
|
+
composer_bottom_rows = @tabs.empty? ? 1 : tab_border_rows(width).length
|
|
563
|
+
footer_rows = footer_text.empty? ? 0 : 1
|
|
564
|
+
attachment_rows = attachment_badge_texts.length
|
|
565
|
+
[height - 7 - composer_bottom_rows - footer_rows - attachment_rows, 4].max
|
|
547
566
|
end
|
|
548
567
|
|
|
549
568
|
def default_project_browser_expanded_paths(paths)
|
|
@@ -40,14 +40,30 @@ module Kward
|
|
|
40
40
|
|
|
41
41
|
def tick_footer_locked
|
|
42
42
|
return false unless @footer && @started && @asking
|
|
43
|
+
return false unless footer_refresh_due?
|
|
43
44
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
previous_text = @cached_footer_text
|
|
46
|
+
refresh_footer_text_locked
|
|
47
|
+
previous_text != @cached_footer_text
|
|
48
|
+
end
|
|
47
49
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
def cached_footer_text
|
|
51
|
+
return "" unless @footer
|
|
52
|
+
|
|
53
|
+
refresh_footer_text_locked if footer_refresh_due?
|
|
54
|
+
@cached_footer_text.to_s
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def footer_refresh_due?
|
|
58
|
+
@last_footer_refresh.nil? || monotonic_now - @last_footer_refresh >= FOOTER_REFRESH_INTERVAL
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def refresh_footer_text_locked
|
|
62
|
+
@cached_footer_text = @footer.call.to_s.gsub(/\s+/, " ").strip
|
|
63
|
+
rescue StandardError
|
|
64
|
+
@cached_footer_text = ""
|
|
65
|
+
ensure
|
|
66
|
+
@last_footer_refresh = monotonic_now
|
|
51
67
|
end
|
|
52
68
|
|
|
53
69
|
def cached_composer_status_text
|
|
@@ -114,6 +114,9 @@ module Kward
|
|
|
114
114
|
@reserved_rows = 0
|
|
115
115
|
@last_width = current_width
|
|
116
116
|
@last_height = current_height
|
|
117
|
+
if interactive_active_locked?
|
|
118
|
+
@interactive_state[:controller].resize(width: interactive_canvas_width(current_width))
|
|
119
|
+
end
|
|
117
120
|
redraw_screen_locked(width: current_width, height: current_height)
|
|
118
121
|
true
|
|
119
122
|
end
|
|
@@ -626,7 +626,13 @@ module Kward
|
|
|
626
626
|
|
|
627
627
|
def selection_matches
|
|
628
628
|
choices = @select_state ? @select_state[:choices] : []
|
|
629
|
-
filter =
|
|
629
|
+
filter = if select_input_active?
|
|
630
|
+
""
|
|
631
|
+
elsif select_search_active? || !composer_input.strip.empty?
|
|
632
|
+
composer_input.downcase.strip
|
|
633
|
+
else
|
|
634
|
+
""
|
|
635
|
+
end
|
|
630
636
|
matches = filter.empty? ? choices : choices.select { |choice| choice.downcase.include?(filter) }
|
|
631
637
|
clamp_selection_index(matches.length)
|
|
632
638
|
matches
|
|
@@ -4,10 +4,14 @@ module Kward
|
|
|
4
4
|
class PromptInterface
|
|
5
5
|
# Bounded in-memory transcript buffer used by the prompt interface.
|
|
6
6
|
class TranscriptBuffer
|
|
7
|
+
TRIM_RATIO = 0.9
|
|
8
|
+
TRIM_BOUNDARY_SEARCH = 1_024
|
|
9
|
+
|
|
7
10
|
attr_reader :text
|
|
8
11
|
|
|
9
12
|
def initialize(limit:)
|
|
10
|
-
@limit = limit
|
|
13
|
+
@limit = [limit.to_i, 1].max
|
|
14
|
+
@trim_target = [(@limit * TRIM_RATIO).floor, 1].max
|
|
11
15
|
@text = +""
|
|
12
16
|
@display_rows_cache_width = nil
|
|
13
17
|
@display_rows_cache = nil
|
|
@@ -42,7 +46,7 @@ module Kward
|
|
|
42
46
|
|
|
43
47
|
def append(text)
|
|
44
48
|
@text << ANSI.sanitize_transcript(text)
|
|
45
|
-
|
|
49
|
+
trim_to_limit if @text.length > @limit
|
|
46
50
|
invalidate_display_rows_cache
|
|
47
51
|
@text
|
|
48
52
|
end
|
|
@@ -77,6 +81,24 @@ module Kward
|
|
|
77
81
|
@display_rows_cache_width = nil
|
|
78
82
|
@display_rows_cache = nil
|
|
79
83
|
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
def trim_to_limit
|
|
88
|
+
cutoff = @text.length - @trim_target
|
|
89
|
+
newline = @text.index("\n", cutoff)
|
|
90
|
+
cutoff = newline + 1 if newline && newline - cutoff <= TRIM_BOUNDARY_SEARCH
|
|
91
|
+
cutoff = safe_escape_boundary(cutoff)
|
|
92
|
+
@text = @text[cutoff..].to_s
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def safe_escape_boundary(cutoff)
|
|
96
|
+
escape_start = @text.rindex("\e", cutoff)
|
|
97
|
+
return cutoff unless escape_start
|
|
98
|
+
|
|
99
|
+
escape = ANSI.escape_sequence_at(@text, escape_start)
|
|
100
|
+
escape && escape_start + escape.length > cutoff ? escape_start + escape.length : cutoff
|
|
101
|
+
end
|
|
80
102
|
end
|
|
81
103
|
end
|
|
82
104
|
end
|
|
@@ -128,12 +128,13 @@ 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, 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)
|
|
132
132
|
@input_io = input
|
|
133
133
|
@output_io = output
|
|
134
134
|
@reader = TTY::Reader.new(input: input, output: output, interrupt: :error)
|
|
135
135
|
@mutex = Mutex.new
|
|
136
136
|
@prompt_history = prompt_history
|
|
137
|
+
@workspace_root = File.expand_path(workspace_root.to_s.empty? ? Dir.pwd : workspace_root)
|
|
137
138
|
@composer = ComposerState.new
|
|
138
139
|
load_history(@prompt_history.values) if @prompt_history
|
|
139
140
|
self.composer_input = @composer.input
|
|
@@ -146,7 +147,8 @@ module Kward
|
|
|
146
147
|
@steered_count = 0
|
|
147
148
|
@spinner_frame_index = 0
|
|
148
149
|
@last_spinner_tick = monotonic_now
|
|
149
|
-
@last_footer_refresh =
|
|
150
|
+
@last_footer_refresh = nil
|
|
151
|
+
@cached_footer_text = nil
|
|
150
152
|
@last_composer_status_refresh = 0.0
|
|
151
153
|
@cached_composer_status_text = nil
|
|
152
154
|
@prompt_label = "You>"
|
|
@@ -340,10 +342,23 @@ module Kward
|
|
|
340
342
|
@mutex.synchronize { editor_active? }
|
|
341
343
|
end
|
|
342
344
|
|
|
343
|
-
def
|
|
345
|
+
def update_workspace_root(root, prompt_history: nil)
|
|
346
|
+
@mutex.synchronize do
|
|
347
|
+
expanded_root = File.expand_path(root.to_s.empty? ? Dir.pwd : root)
|
|
348
|
+
return false if expanded_root == @workspace_root
|
|
349
|
+
|
|
350
|
+
@workspace_root = expanded_root
|
|
351
|
+
@prompt_history = prompt_history if prompt_history
|
|
352
|
+
load_history(@prompt_history.values) if @prompt_history
|
|
353
|
+
reset_workspace_file_state_locked
|
|
354
|
+
true
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def edit_file(path, base_dir: nil, allow_new: true)
|
|
344
359
|
start(render: false)
|
|
345
360
|
opened = @mutex.synchronize do
|
|
346
|
-
open_editor(path, allow_new: allow_new, base_dir: base_dir, restrict_to_workspace: false).tap do
|
|
361
|
+
open_editor(path, allow_new: allow_new, base_dir: base_dir || prompt_workspace_root, restrict_to_workspace: false).tap do
|
|
347
362
|
render_prompt_locked
|
|
348
363
|
end
|
|
349
364
|
end
|
|
@@ -610,9 +625,14 @@ module Kward
|
|
|
610
625
|
|
|
611
626
|
def update_tabs(labels:, active_index: 0)
|
|
612
627
|
@mutex.synchronize do
|
|
613
|
-
|
|
614
|
-
|
|
628
|
+
normalized_labels = Array(labels).map { |label| normalize_tab_label(label) }
|
|
629
|
+
normalized_index = active_index.to_i
|
|
630
|
+
return false if @tabs == normalized_labels && @active_tab_index == normalized_index
|
|
631
|
+
|
|
632
|
+
@tabs = normalized_labels
|
|
633
|
+
@active_tab_index = normalized_index
|
|
615
634
|
render_prompt_locked if @started && @asking
|
|
635
|
+
true
|
|
616
636
|
end
|
|
617
637
|
end
|
|
618
638
|
|
|
@@ -625,16 +645,19 @@ module Kward
|
|
|
625
645
|
end
|
|
626
646
|
end
|
|
627
647
|
|
|
628
|
-
def tab_view_snapshot
|
|
648
|
+
def tab_view_snapshot(include_transcript: true)
|
|
629
649
|
@mutex.synchronize do
|
|
630
|
-
{
|
|
650
|
+
snapshot = {
|
|
631
651
|
composer: @composer.dup,
|
|
632
652
|
prompt_label: @prompt_label.dup,
|
|
633
|
-
editor_state: @editor_state&.dup
|
|
634
|
-
transcript_buffer: @transcript_buffer.dup,
|
|
635
|
-
transcript_viewport_rows: @transcript_viewport_rows,
|
|
636
|
-
stream_state: @stream_state.dup
|
|
653
|
+
editor_state: @editor_state&.dup
|
|
637
654
|
}
|
|
655
|
+
if include_transcript
|
|
656
|
+
snapshot[:transcript_buffer] = @transcript_buffer.dup
|
|
657
|
+
snapshot[:transcript_viewport_rows] = @transcript_viewport_rows
|
|
658
|
+
snapshot[:stream_state] = @stream_state.dup
|
|
659
|
+
end
|
|
660
|
+
snapshot
|
|
638
661
|
end
|
|
639
662
|
end
|
|
640
663
|
|
|
@@ -859,6 +882,8 @@ module Kward
|
|
|
859
882
|
|
|
860
883
|
def refresh_composer_status
|
|
861
884
|
@mutex.synchronize do
|
|
885
|
+
@cached_footer_text = nil
|
|
886
|
+
@last_footer_refresh = nil
|
|
862
887
|
@cached_composer_status_text = nil
|
|
863
888
|
@last_composer_status_refresh = 0.0
|
|
864
889
|
render_prompt_locked if @started && @asking
|
|
@@ -879,6 +904,22 @@ module Kward
|
|
|
879
904
|
|
|
880
905
|
private
|
|
881
906
|
|
|
907
|
+
def prompt_workspace_root
|
|
908
|
+
@workspace_root
|
|
909
|
+
end
|
|
910
|
+
|
|
911
|
+
def reset_workspace_file_state_locked
|
|
912
|
+
@file_mention_paths = nil
|
|
913
|
+
@file_mention_path_entries_paths = nil
|
|
914
|
+
@file_mention_path_entries = nil
|
|
915
|
+
@project_browser_state = nil
|
|
916
|
+
@project_browser_tree_paths = nil
|
|
917
|
+
@project_browser_tree = nil
|
|
918
|
+
@file_overlay_dismissed_token = nil
|
|
919
|
+
@file_open_dismissed_token = nil
|
|
920
|
+
@file_editor_open_status = nil
|
|
921
|
+
end
|
|
922
|
+
|
|
882
923
|
def modal_active_locked?
|
|
883
924
|
@question_prompt_active || !@question_state.nil? || !@select_state.nil? || !@git_state.nil?
|
|
884
925
|
end
|
|
@@ -30,7 +30,7 @@ module Kward
|
|
|
30
30
|
{ name: "shell", description: "Open the embedded Kward shell.", argument_hint: "" },
|
|
31
31
|
{ name: "scratchpad", description: "Open an unsaved editor buffer.", argument_hint: "[text|markdown|ruby]" },
|
|
32
32
|
{ name: "pty", description: "Run a command in an interactive PTY passthrough session.", argument_hint: "<command>" },
|
|
33
|
-
{ name: "tab", description: "Manage tabs.", argument_hint: "[1-n|move|close|new|name]" },
|
|
33
|
+
{ name: "tab", description: "Manage tabs.", argument_hint: "[1-n|move|close|new|name|worktree]" },
|
|
34
34
|
{ name: "status", description: "Show the current status message.", argument_hint: "" },
|
|
35
35
|
{ name: "stats", description: "Show telemetry logging stats.", argument_hint: "[range]" },
|
|
36
36
|
{ name: "sandbox", description: "Inspect or configure model command sandboxing.", argument_hint: "[status|off|read_only|workspace_write|network allow|network deny]" },
|
|
@@ -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,25 +22,26 @@ 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)
|
|
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)
|
|
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).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).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)
|
|
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).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)
|
|
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] }
|
|
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)
|
|
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)
|
|
41
41
|
return replacement_prompt_sections if ConfigFiles.replacement_system_prompt?
|
|
42
42
|
|
|
43
43
|
sections = [prompt_section("Built-in system prompt", base_prompt)]
|
|
44
|
+
sections << prompt_section("Execution profile", execution_profile_context) unless execution_profile_context.to_s.empty?
|
|
44
45
|
sections << prompt_section(config_agents_prompt_label, config_agents_prompt, source: config_agents_prompt_source)
|
|
45
46
|
sections << prompt_section("Memory context", memory_context) unless memory_context.to_s.empty?
|
|
46
47
|
if include_workspace_personality
|