kward 0.71.0 → 0.73.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/.github/workflows/ci.yml +30 -0
- data/CHANGELOG.md +93 -0
- data/Gemfile.lock +2 -2
- data/README.md +4 -0
- data/doc/agent-tools.md +15 -6
- data/doc/authentication.md +22 -1
- data/doc/code-search.md +42 -2
- data/doc/configuration.md +106 -3
- data/doc/context-budgeting.md +136 -0
- data/doc/context-tools.md +16 -3
- data/doc/editor.md +415 -0
- data/doc/extensibility.md +16 -7
- data/doc/files.md +100 -0
- data/doc/getting-started.md +25 -18
- data/doc/git.md +123 -0
- data/doc/memory.md +24 -4
- data/doc/personas.md +34 -5
- data/doc/plugins.md +72 -1
- data/doc/releasing.md +37 -9
- data/doc/rpc.md +75 -5
- data/doc/session-management.md +35 -1
- data/doc/shell.md +332 -0
- data/doc/tabs.md +122 -0
- data/doc/troubleshooting.md +77 -1
- data/doc/usage.md +79 -7
- data/doc/web-search.md +12 -4
- data/doc/workspace-tools.md +51 -12
- data/examples/plugins/space_invaders.rb +377 -0
- data/lib/kward/agent.rb +1 -1
- data/lib/kward/ansi.rb +62 -23
- data/lib/kward/cli/commands.rb +33 -2
- data/lib/kward/cli/git.rb +150 -0
- data/lib/kward/cli/interactive_turn.rb +73 -9
- data/lib/kward/cli/plugins.rb +54 -4
- data/lib/kward/cli/prompt_interface.rb +32 -1
- data/lib/kward/cli/rendering.rb +4 -1
- data/lib/kward/cli/runtime_helpers.rb +268 -4
- data/lib/kward/cli/sessions.rb +2 -2
- data/lib/kward/cli/settings.rb +217 -9
- data/lib/kward/cli/slash_commands.rb +628 -2
- data/lib/kward/cli/tabs.rb +725 -0
- data/lib/kward/cli/tool_summaries.rb +6 -0
- data/lib/kward/cli.rb +150 -26
- data/lib/kward/clipboard.rb +2 -3
- data/lib/kward/compactor.rb +7 -19
- data/lib/kward/config_files.rb +145 -1
- data/lib/kward/context_budget_meter.rb +44 -0
- data/lib/kward/conversation.rb +12 -4
- data/lib/kward/editor_mode.rb +25 -0
- data/lib/kward/ekwsh.rb +559 -0
- data/lib/kward/image_attachments.rb +3 -1
- data/lib/kward/interactive_pty_runner.rb +151 -0
- data/lib/kward/local_command_runner.rb +155 -0
- data/lib/kward/local_pty_command_runner.rb +171 -0
- data/lib/kward/model/context_usage.rb +2 -2
- data/lib/kward/model/payloads.rb +2 -5
- data/lib/kward/plugin_registry.rb +61 -0
- data/lib/kward/project_files.rb +52 -0
- data/lib/kward/prompt_history.rb +84 -0
- data/lib/kward/prompt_interface/composer_controller.rb +69 -1
- data/lib/kward/prompt_interface/composer_renderer.rb +109 -13
- data/lib/kward/prompt_interface/composer_state.rb +96 -27
- data/lib/kward/prompt_interface/editor/auto_close_pairs.rb +123 -0
- data/lib/kward/prompt_interface/editor/auto_indent.rb +510 -0
- data/lib/kward/prompt_interface/editor/buffer.rb +109 -0
- data/lib/kward/prompt_interface/editor/controller.rb +1218 -0
- data/lib/kward/prompt_interface/editor/endwise.rb +321 -0
- data/lib/kward/prompt_interface/editor/file_marker.rb +40 -0
- data/lib/kward/prompt_interface/editor/indent_navigation.rb +61 -0
- data/lib/kward/prompt_interface/editor/kill_ring.rb +78 -0
- data/lib/kward/prompt_interface/editor/modes/emacs.rb +259 -0
- data/lib/kward/prompt_interface/editor/modes/modern.rb +354 -0
- data/lib/kward/prompt_interface/editor/modes/vibe.rb +1812 -0
- data/lib/kward/prompt_interface/editor/modes/vibe_insert_readline.rb +166 -0
- data/lib/kward/prompt_interface/editor/renderer.rb +244 -0
- data/lib/kward/prompt_interface/editor/search.rb +76 -0
- data/lib/kward/prompt_interface/editor/selections.rb +120 -0
- data/lib/kward/prompt_interface/editor/state.rb +1271 -0
- data/lib/kward/prompt_interface/editor/status_text.rb +23 -0
- data/lib/kward/prompt_interface/editor/syntax_highlighter.rb +422 -0
- data/lib/kward/prompt_interface/editor/undo_history.rb +46 -0
- data/lib/kward/prompt_interface/editor/vibe_state.rb +44 -0
- data/lib/kward/prompt_interface/file_overlay.rb +211 -0
- data/lib/kward/prompt_interface/git_prompt.rb +288 -0
- data/lib/kward/prompt_interface/interactive/controller.rb +186 -0
- data/lib/kward/prompt_interface/interactive/renderer.rb +71 -0
- data/lib/kward/prompt_interface/interactive/state.rb +62 -0
- data/lib/kward/prompt_interface/key_handler.rb +451 -57
- data/lib/kward/prompt_interface/overlay_renderer.rb +21 -2
- data/lib/kward/prompt_interface/project_browser.rb +524 -0
- data/lib/kward/prompt_interface/question_prompt.rb +99 -56
- data/lib/kward/prompt_interface/runtime_state.rb +43 -0
- data/lib/kward/prompt_interface/screen.rb +19 -3
- data/lib/kward/prompt_interface/selection_prompt.rb +10 -19
- data/lib/kward/prompt_interface/slash_overlay.rb +2 -0
- data/lib/kward/prompt_interface/stream_state.rb +7 -0
- data/lib/kward/prompt_interface/transcript_buffer.rb +6 -0
- data/lib/kward/prompt_interface.rb +366 -222
- data/lib/kward/prompts/commands.rb +9 -0
- data/lib/kward/prompts.rb +2 -0
- data/lib/kward/rpc/memory_methods.rb +83 -0
- data/lib/kward/rpc/server.rb +169 -83
- data/lib/kward/rpc/session_manager.rb +45 -121
- data/lib/kward/rpc/session_tree_rows.rb +9 -115
- data/lib/kward/rpc/tool_event_normalizer.rb +1 -1
- data/lib/kward/rpc/tool_metadata.rb +11 -0
- data/lib/kward/rpc/transcript_normalizer.rb +4 -39
- data/lib/kward/scratchpad_runner.rb +56 -0
- data/lib/kward/session_diff.rb +20 -3
- data/lib/kward/session_naming.rb +11 -0
- data/lib/kward/session_store.rb +44 -0
- data/lib/kward/session_tree_nodes.rb +136 -0
- data/lib/kward/session_tree_renderer.rb +9 -131
- data/lib/kward/tab_store.rb +47 -0
- data/lib/kward/terminal_keys.rb +84 -0
- data/lib/kward/terminal_sequences.rb +42 -0
- data/lib/kward/text_boundary.rb +25 -0
- data/lib/kward/tools/context_budget_stats.rb +54 -0
- data/lib/kward/tools/context_for_task.rb +204 -0
- data/lib/kward/tools/read_file.rb +8 -4
- data/lib/kward/tools/registry.rb +62 -16
- data/lib/kward/tools/tool_call.rb +10 -0
- data/lib/kward/version.rb +1 -1
- data/lib/kward/workers/git_guard.rb +93 -0
- data/lib/kward/workers/job.rb +99 -0
- data/lib/kward/workers/live_view.rb +49 -0
- data/lib/kward/workers/manager.rb +288 -0
- data/lib/kward/workers/queue_runner.rb +166 -0
- data/lib/kward/workers/queue_store.rb +112 -0
- data/lib/kward/workers/store.rb +72 -0
- data/lib/kward/workers/tool_policy.rb +23 -0
- data/lib/kward/workers/worker.rb +82 -0
- data/lib/kward/workers/write_lock.rb +38 -0
- data/lib/kward/workers.rb +10 -0
- data/lib/kward/workspace.rb +125 -87
- data/templates/default/fulldoc/html/css/kward.css +140 -36
- data/templates/default/fulldoc/html/images/kward_screen_1.png +0 -0
- data/templates/default/fulldoc/html/setup.rb +1 -0
- data/templates/default/kward_navigation.rb +12 -1
- data/templates/default/layout/html/layout.erb +23 -34
- data/templates/default/layout/html/setup.rb +6 -0
- metadata +67 -1
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
require "open3"
|
|
2
|
+
|
|
3
|
+
# Namespace for the Kward CLI agent runtime.
|
|
4
|
+
module Kward
|
|
5
|
+
# Command-line frontend that coordinates terminal interaction, sessions, tools, and model turns.
|
|
6
|
+
class CLI
|
|
7
|
+
# Interactive Git status and commit helpers.
|
|
8
|
+
module GitCommands
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def handle_git_command(agent)
|
|
12
|
+
unless @prompt.respond_to?(:git_commit_message)
|
|
13
|
+
runtime_output("/git is available in the interactive overlay only.")
|
|
14
|
+
return
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
root = interactive_workspace_root(agent)
|
|
18
|
+
git_root = git_repository_root(root)
|
|
19
|
+
if git_root.empty?
|
|
20
|
+
runtime_output("Not a Git repository: #{root}")
|
|
21
|
+
return
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
status = git_status_lines(git_root)
|
|
25
|
+
message = @prompt.git_commit_message(status) do |action|
|
|
26
|
+
result = handle_git_prompt_action(git_root, status, action)
|
|
27
|
+
status = result.is_a?(Hash) && result.key?(:status_lines) ? result[:status_lines] : result
|
|
28
|
+
result
|
|
29
|
+
end
|
|
30
|
+
return if message.nil?
|
|
31
|
+
|
|
32
|
+
result = run_busy_local_command_and_requeue(activity: "committing") do
|
|
33
|
+
git_commit(git_root, message)
|
|
34
|
+
end
|
|
35
|
+
print_git_commit_result(result)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def git_repository_root(root)
|
|
39
|
+
output, status = Open3.capture2e("git", "rev-parse", "--show-toplevel", chdir: root.to_s)
|
|
40
|
+
return "" unless status.success?
|
|
41
|
+
|
|
42
|
+
output.lines.first.to_s.strip
|
|
43
|
+
rescue StandardError
|
|
44
|
+
""
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def git_status_lines(root)
|
|
48
|
+
output, status = Open3.capture2e("git", "status", "--short", "--untracked-files=normal", chdir: root.to_s)
|
|
49
|
+
return ["Unable to read Git status: #{output.strip}"] unless status.success?
|
|
50
|
+
|
|
51
|
+
output.lines.map(&:chomp)
|
|
52
|
+
rescue StandardError => e
|
|
53
|
+
["Unable to read Git status: #{e.message}"]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def handle_git_prompt_action(root, current_status, action)
|
|
57
|
+
case action[:action]
|
|
58
|
+
when :toggle_stage
|
|
59
|
+
toggle_git_stage(root, current_status[action[:index].to_i])
|
|
60
|
+
git_status_lines(root)
|
|
61
|
+
when :open_diff
|
|
62
|
+
status_line = current_status[action[:index].to_i]
|
|
63
|
+
{ status_lines: git_status_lines(root), diff: git_diff_view(root, status_line) }
|
|
64
|
+
else
|
|
65
|
+
git_status_lines(root)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def git_diff_view(root, status_line)
|
|
70
|
+
entry = parse_git_status_line(status_line)
|
|
71
|
+
return { path: "Git diff", content: "Unable to read Git status entry.\n" } if entry.nil?
|
|
72
|
+
|
|
73
|
+
output = entry[:untracked] ? git_untracked_file_diff(root, entry[:path]) : git_tracked_file_diff(root, entry[:path])
|
|
74
|
+
{ path: entry[:path], content: output.empty? ? "No diff for #{entry[:path]}\n" : output }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def git_tracked_file_diff(root, path)
|
|
78
|
+
output, status = Open3.capture2e("git", "diff", "HEAD", "--", path, chdir: root.to_s)
|
|
79
|
+
status.success? ? output : "Unable to read diff for #{path}:\n#{output}"
|
|
80
|
+
rescue StandardError => e
|
|
81
|
+
"Unable to read diff for #{path}: #{e.message}\n"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def git_untracked_file_diff(root, path)
|
|
85
|
+
full_path = File.expand_path(path, root.to_s)
|
|
86
|
+
content = File.file?(full_path) ? File.read(full_path) : ""
|
|
87
|
+
lines = ["diff --git a/#{path} b/#{path}", "new file mode 100644", "--- /dev/null", "+++ b/#{path}", "@@ -0,0 +1,#{content.lines.length} @@"]
|
|
88
|
+
lines.concat(content.lines(chomp: true).map { |line| "+#{line}" })
|
|
89
|
+
lines << "\" if !content.empty? && !content.end_with?("\n")
|
|
90
|
+
lines.join("\n") + "\n"
|
|
91
|
+
rescue StandardError => e
|
|
92
|
+
"Unable to read diff for #{path}: #{e.message}\n"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def toggle_git_stage(root, status_line)
|
|
96
|
+
entry = parse_git_status_line(status_line)
|
|
97
|
+
return if entry.nil?
|
|
98
|
+
|
|
99
|
+
command = entry[:staged] ? ["restore", "--staged", "--", entry[:path]] : ["add", "--", entry[:path]]
|
|
100
|
+
Open3.capture2e("git", *command, chdir: root.to_s)
|
|
101
|
+
rescue StandardError
|
|
102
|
+
nil
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def parse_git_status_line(line)
|
|
106
|
+
text = line.to_s
|
|
107
|
+
return nil if text.length < 4
|
|
108
|
+
|
|
109
|
+
status = text[0, 2]
|
|
110
|
+
path = text[3..].to_s
|
|
111
|
+
path = path.split(" -> ", 2).last if status.include?("R") || status.include?("C")
|
|
112
|
+
return nil if path.empty?
|
|
113
|
+
|
|
114
|
+
{ path: path, staged: status[0] != " " && status[0] != "?", untracked: status == "??" }
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def git_commit(root, message)
|
|
118
|
+
return git_commit_staged(root, message) if git_staged_changes?(root)
|
|
119
|
+
|
|
120
|
+
add_output, add_status = Open3.capture2e("git", "add", "--all", chdir: root.to_s)
|
|
121
|
+
return { success: false, output: add_output } unless add_status.success?
|
|
122
|
+
|
|
123
|
+
git_commit_staged(root, message)
|
|
124
|
+
rescue StandardError => e
|
|
125
|
+
{ success: false, output: e.message }
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def git_staged_changes?(root)
|
|
129
|
+
_output, status = Open3.capture2e("git", "diff", "--cached", "--quiet", chdir: root.to_s)
|
|
130
|
+
!status.success?
|
|
131
|
+
rescue StandardError
|
|
132
|
+
false
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def git_commit_staged(root, message)
|
|
136
|
+
commit_output, commit_status = Open3.capture2e("git", "commit", "-m", message.to_s, chdir: root.to_s)
|
|
137
|
+
{ success: commit_status.success?, output: commit_output }
|
|
138
|
+
rescue StandardError => e
|
|
139
|
+
{ success: false, output: e.message }
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def print_git_commit_result(result)
|
|
143
|
+
output = result[:output].to_s.strip
|
|
144
|
+
output = result[:success] ? "Commit created." : "Git commit failed." if output.empty?
|
|
145
|
+
status = result[:success] ? "Git commit succeeded" : "Git commit failed"
|
|
146
|
+
runtime_output("#{status}\n#{output}")
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
@@ -7,6 +7,7 @@ module Kward
|
|
|
7
7
|
private
|
|
8
8
|
|
|
9
9
|
def run_interactive_turn(agent, input, display_input: nil)
|
|
10
|
+
stop_live_worker_view if respond_to?(:stop_live_worker_view, true)
|
|
10
11
|
prepare_memory_context(agent.conversation, input) if agent.respond_to?(:conversation)
|
|
11
12
|
print_user_transcript(input, display_input: display_input) if prompt_interface?
|
|
12
13
|
return run_blocking_interactive_turn(agent, input, display_input: display_input) unless prompt_interface?
|
|
@@ -42,7 +43,7 @@ module Kward
|
|
|
42
43
|
|
|
43
44
|
while worker.alive?
|
|
44
45
|
begin
|
|
45
|
-
poll_result = collect_busy_input(queued_inputs, steering)
|
|
46
|
+
poll_result = collect_busy_input(queued_inputs, steering, agent)
|
|
46
47
|
sleep 0.01
|
|
47
48
|
rescue Interrupt
|
|
48
49
|
poll_result = PromptInterface::CANCEL_INPUT
|
|
@@ -52,7 +53,11 @@ module Kward
|
|
|
52
53
|
cancellation.cancel!
|
|
53
54
|
worker.raise(Cancellation::CancelledError, "cancelled") if worker.alive?
|
|
54
55
|
end
|
|
55
|
-
|
|
56
|
+
if busy_replacement_agent?
|
|
57
|
+
discard_interactive_events(event_queue, markdown_chunks, stream_state)
|
|
58
|
+
else
|
|
59
|
+
drain_interactive_events(event_queue, markdown_chunks, stream_state, agent)
|
|
60
|
+
end
|
|
56
61
|
end
|
|
57
62
|
begin
|
|
58
63
|
worker.join
|
|
@@ -60,10 +65,14 @@ module Kward
|
|
|
60
65
|
error ||= e
|
|
61
66
|
end
|
|
62
67
|
drain_busy_input(queued_inputs, nil) unless cancelled
|
|
63
|
-
|
|
64
|
-
|
|
68
|
+
if busy_replacement_agent?
|
|
69
|
+
discard_interactive_events(event_queue, markdown_chunks, stream_state, force: true)
|
|
70
|
+
else
|
|
71
|
+
drain_interactive_events(event_queue, markdown_chunks, stream_state, agent, force: true)
|
|
72
|
+
end
|
|
73
|
+
raise error if error && !error.is_a?(Cancellation::CancelledError) && !busy_replacement_agent?
|
|
65
74
|
|
|
66
|
-
@prompt.say("\n#{colored(assistant_output_prompt, :green, :bold)} #{render_markdown_transcript(answer)}\n") unless cancelled || stream_state[:streamed] || answer.to_s.empty?
|
|
75
|
+
@prompt.say("\n#{colored(assistant_output_prompt, :green, :bold)} #{render_markdown_transcript(answer)}\n") unless cancelled || busy_replacement_agent? || stream_state[:streamed] || answer.to_s.empty?
|
|
67
76
|
persist_memory_state(agent.conversation) if agent.respond_to?(:conversation)
|
|
68
77
|
auto_summarize_memory(agent.conversation) if agent.respond_to?(:conversation) && queued_inputs.empty? && !cancelled
|
|
69
78
|
queued_inputs
|
|
@@ -87,6 +96,21 @@ module Kward
|
|
|
87
96
|
flush_interactive_markdown_deltas(markdown_chunks, stream_state, force: force)
|
|
88
97
|
end
|
|
89
98
|
|
|
99
|
+
def discard_interactive_events(event_queue, markdown_chunks, stream_state, force: false)
|
|
100
|
+
drained = 0
|
|
101
|
+
loop do
|
|
102
|
+
break if !force && drained >= INTERACTIVE_EVENT_DRAIN_LIMIT
|
|
103
|
+
|
|
104
|
+
event_queue.pop(true)
|
|
105
|
+
drained += 1
|
|
106
|
+
rescue ThreadError
|
|
107
|
+
break
|
|
108
|
+
end
|
|
109
|
+
markdown_chunks.clear
|
|
110
|
+
finish_stream_block if stream_state[:stream_block_open]
|
|
111
|
+
stream_state[:stream_block_open] = false
|
|
112
|
+
end
|
|
113
|
+
|
|
90
114
|
def handle_interactive_event(event, markdown_chunks, stream_state)
|
|
91
115
|
case event
|
|
92
116
|
when Events::ReasoningDelta
|
|
@@ -154,13 +178,18 @@ module Kward
|
|
|
154
178
|
collect_busy_input(queued_inputs, nil)
|
|
155
179
|
end
|
|
156
180
|
|
|
157
|
-
def collect_busy_input(queued_inputs, steering)
|
|
181
|
+
def collect_busy_input(queued_inputs, steering, agent = nil)
|
|
158
182
|
return nil if @prompt.respond_to?(:modal_active?) && @prompt.modal_active?
|
|
159
183
|
|
|
160
184
|
poll_result = @prompt.poll_input
|
|
161
185
|
case poll_result
|
|
162
186
|
when String
|
|
163
|
-
if
|
|
187
|
+
return poll_result if handle_busy_worker_input(poll_result, agent, queued_inputs)
|
|
188
|
+
|
|
189
|
+
if slash_command_input?(poll_result)
|
|
190
|
+
queued_inputs << poll_result
|
|
191
|
+
@prompt.set_queued_count(queued_inputs.length) if @prompt.respond_to?(:set_queued_count)
|
|
192
|
+
elsif steering && !poll_result.strip.empty?
|
|
164
193
|
begin
|
|
165
194
|
steering.submit(poll_result)
|
|
166
195
|
@prompt.set_steered_count(1) if @prompt.respond_to?(:set_steered_count)
|
|
@@ -183,16 +212,51 @@ module Kward
|
|
|
183
212
|
drain_busy_input(queued_inputs, nil)
|
|
184
213
|
end
|
|
185
214
|
|
|
186
|
-
def drain_busy_input(queued_inputs, steering)
|
|
215
|
+
def drain_busy_input(queued_inputs, steering, agent = nil)
|
|
187
216
|
deadline = Time.now + 0.15
|
|
188
217
|
loop do
|
|
189
|
-
poll_result = collect_busy_input(queued_inputs, steering)
|
|
218
|
+
poll_result = collect_busy_input(queued_inputs, steering, agent)
|
|
190
219
|
break if Time.now > deadline && poll_result.nil?
|
|
191
220
|
|
|
192
221
|
sleep 0.01
|
|
193
222
|
end
|
|
194
223
|
end
|
|
195
224
|
|
|
225
|
+
def slash_command_input?(input)
|
|
226
|
+
input.to_s.strip.start_with?("/")
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def handle_busy_worker_input(input, agent, queued_inputs)
|
|
230
|
+
return false unless agent
|
|
231
|
+
|
|
232
|
+
command = input.to_s.strip
|
|
233
|
+
return false unless command == "/workers" || command.start_with?("/workers ")
|
|
234
|
+
|
|
235
|
+
_handled, replacement_agent = handle_local_slash_command(command, agent, @session_store)
|
|
236
|
+
@busy_replacement_agent = replacement_agent if replacement_agent?(replacement_agent)
|
|
237
|
+
restore_busy_input_prompt
|
|
238
|
+
true
|
|
239
|
+
rescue StandardError => e
|
|
240
|
+
runtime_output("Error: #{e.message}")
|
|
241
|
+
restore_busy_input_prompt
|
|
242
|
+
true
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def replacement_agent?(object)
|
|
246
|
+
object.respond_to?(:conversation) && object.respond_to?(:ask)
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def busy_replacement_agent?
|
|
250
|
+
replacement_agent?(@busy_replacement_agent)
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def restore_busy_input_prompt
|
|
254
|
+
return unless @prompt.respond_to?(:begin_busy_input)
|
|
255
|
+
return if @prompt.respond_to?(:modal_active?) && @prompt.modal_active?
|
|
256
|
+
|
|
257
|
+
@prompt.begin_busy_input("You>")
|
|
258
|
+
end
|
|
259
|
+
|
|
196
260
|
def steering_supported?
|
|
197
261
|
@client.respond_to?(:supports_in_flight_steer?) && @client.supports_in_flight_steer?
|
|
198
262
|
end
|
data/lib/kward/cli/plugins.rb
CHANGED
|
@@ -7,7 +7,7 @@ module Kward
|
|
|
7
7
|
private
|
|
8
8
|
|
|
9
9
|
def prompt_templates
|
|
10
|
-
@prompt_templates ||= ConfigFiles.prompt_templates(reserved_commands:
|
|
10
|
+
@prompt_templates ||= ConfigFiles.prompt_templates(reserved_commands: builtin_slash_command_names)
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def plugin_registry
|
|
@@ -22,6 +22,14 @@ module Kward
|
|
|
22
22
|
plugin_registry.command_for(command)
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
+
def interactive_commands
|
|
26
|
+
plugin_registry.interactive_commands
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def interactive_command_for(command)
|
|
30
|
+
plugin_registry.interactive_command_for(command)
|
|
31
|
+
end
|
|
32
|
+
|
|
25
33
|
def reload_plugins(conversation)
|
|
26
34
|
@plugin_registry = PluginRegistry.load(reserved_commands: reserved_slash_command_names)
|
|
27
35
|
conversation.plugin_registry = @plugin_registry if conversation.respond_to?(:plugin_registry=)
|
|
@@ -30,7 +38,34 @@ module Kward
|
|
|
30
38
|
end
|
|
31
39
|
|
|
32
40
|
def reserved_slash_command_names
|
|
33
|
-
|
|
41
|
+
builtin_slash_command_names + prompt_templates.map(&:command)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def run_interactive_command(name, argument, agent)
|
|
45
|
+
command = interactive_command_for(name)
|
|
46
|
+
return [false, nil] unless command
|
|
47
|
+
return [false, nil] unless prompt_interface? && @prompt.respond_to?(:start_interactive)
|
|
48
|
+
|
|
49
|
+
context = plugin_context(agent.conversation, argument)
|
|
50
|
+
controller = @prompt.start_interactive(title: "/#{name}", rows: command.rows, fps: command.fps)
|
|
51
|
+
command.handler.call(controller, context)
|
|
52
|
+
run_interactive_loop
|
|
53
|
+
[true, nil]
|
|
54
|
+
rescue StandardError => e
|
|
55
|
+
@prompt.finish_interactive if @prompt.respond_to?(:finish_interactive)
|
|
56
|
+
runtime_output("Interactive command /#{name} error: #{e.message}")
|
|
57
|
+
[true, nil]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def run_interactive_loop
|
|
61
|
+
loop do
|
|
62
|
+
result = @prompt.poll_input
|
|
63
|
+
if result == :interactive_exited || @prompt.interactive_exited?
|
|
64
|
+
@prompt.finish_interactive
|
|
65
|
+
break
|
|
66
|
+
end
|
|
67
|
+
sleep 0.01
|
|
68
|
+
end
|
|
34
69
|
end
|
|
35
70
|
|
|
36
71
|
def slash_command_entries
|
|
@@ -42,15 +77,30 @@ module Kward
|
|
|
42
77
|
}
|
|
43
78
|
end
|
|
44
79
|
plugin_entries = plugin_commands.map(&:entry)
|
|
45
|
-
|
|
80
|
+
interactive_entries = interactive_commands.map(&:entry)
|
|
81
|
+
builtin_slash_commands + prompt_entries + plugin_entries + interactive_entries
|
|
46
82
|
end
|
|
47
83
|
|
|
48
84
|
def prompt_template_for(command)
|
|
49
85
|
prompt_templates.find { |template| template.command == command }
|
|
50
86
|
end
|
|
51
87
|
|
|
88
|
+
def builtin_slash_commands
|
|
89
|
+
return BUILTIN_SLASH_COMMANDS if experimental_workers_enabled?
|
|
90
|
+
|
|
91
|
+
BUILTIN_SLASH_COMMANDS.reject { |command| %w[workers queue].include?(command[:name]) }
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def builtin_slash_command_names
|
|
95
|
+
builtin_slash_commands.map { |command| command[:name] }
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def experimental_workers_enabled?
|
|
99
|
+
@experimental_workers == true
|
|
100
|
+
end
|
|
101
|
+
|
|
52
102
|
def expand_prompt_template(input)
|
|
53
|
-
PromptCommands.expand(input, templates: prompt_templates, reserved_commands:
|
|
103
|
+
PromptCommands.expand(input, templates: prompt_templates, reserved_commands: builtin_slash_command_names)
|
|
54
104
|
end
|
|
55
105
|
|
|
56
106
|
def run_plugin_command(name, argument, agent)
|
|
@@ -23,7 +23,21 @@ module Kward
|
|
|
23
23
|
busy_help: ConfigFiles.composer_busy_help?,
|
|
24
24
|
attachment_badges: method(:composer_attachment_badges),
|
|
25
25
|
attachment_parser: method(:composer_attachment_parser),
|
|
26
|
-
banner_message: Kward::PromptInterface::BANNER_MESSAGE
|
|
26
|
+
banner_message: Kward::PromptInterface::BANNER_MESSAGE,
|
|
27
|
+
tab_keybindings: ConfigFiles.composer_tab_keybindings,
|
|
28
|
+
prompt_history: PromptHistory.new(cwd: current_workspace_root),
|
|
29
|
+
editor_mode: ConfigFiles.editor_mode,
|
|
30
|
+
editor_mode_source: -> { ConfigFiles.editor_mode },
|
|
31
|
+
editor_auto_indent: ConfigFiles.editor_auto_indent?,
|
|
32
|
+
editor_auto_indent_source: -> { ConfigFiles.editor_auto_indent? },
|
|
33
|
+
editor_auto_close_pairs: ConfigFiles.editor_auto_close_pairs?,
|
|
34
|
+
editor_auto_close_pairs_source: -> { ConfigFiles.editor_auto_close_pairs? },
|
|
35
|
+
editor_soft_wrap: ConfigFiles.editor_soft_wrap?,
|
|
36
|
+
editor_soft_wrap_source: -> { ConfigFiles.editor_soft_wrap? },
|
|
37
|
+
editor_bar_cursor: ConfigFiles.editor_bar_cursor?,
|
|
38
|
+
editor_bar_cursor_source: -> { ConfigFiles.editor_bar_cursor? },
|
|
39
|
+
editor_line_numbers: ConfigFiles.editor_line_numbers,
|
|
40
|
+
editor_line_numbers_source: -> { ConfigFiles.editor_line_numbers }
|
|
27
41
|
)
|
|
28
42
|
if @prompt.method(:start).parameters.any? { |kind, name| [:key, :keyreq].include?(kind) && name == :render }
|
|
29
43
|
@prompt.start(render: false)
|
|
@@ -149,6 +163,8 @@ module Kward
|
|
|
149
163
|
reasoning = "n/a" unless ModelInfo.reasoning_supported?(provider, model) && !reasoning.to_s.empty?
|
|
150
164
|
text = "#{provider} #{model} · #{reasoning}"
|
|
151
165
|
parts = []
|
|
166
|
+
git = composer_git_branch_text
|
|
167
|
+
parts << git if git
|
|
152
168
|
diff = composer_session_diff_text
|
|
153
169
|
parts << diff if diff
|
|
154
170
|
usage = composer_context_usage(provider, model)
|
|
@@ -165,6 +181,21 @@ module Kward
|
|
|
165
181
|
"#{additions}|#{deletions}"
|
|
166
182
|
end
|
|
167
183
|
|
|
184
|
+
def composer_git_branch_text
|
|
185
|
+
git_root = startup_git_root(current_workspace_root)
|
|
186
|
+
return nil if git_root.to_s.empty?
|
|
187
|
+
|
|
188
|
+
branch = startup_git_output(%w[git branch --show-current], root: git_root)
|
|
189
|
+
branch = startup_git_output(%w[git rev-parse --short HEAD], root: git_root) if branch.empty?
|
|
190
|
+
branch = "unknown" if branch.empty?
|
|
191
|
+
color = composer_git_dirty?(git_root) ? :yellow : nil
|
|
192
|
+
ANSI.colorize(branch, color, enabled: @color_enabled)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def composer_git_dirty?(git_root)
|
|
196
|
+
!startup_git_output(%w[git status --porcelain --untracked-files=normal], root: git_root).empty?
|
|
197
|
+
end
|
|
198
|
+
|
|
168
199
|
def composer_context_percent_text(percent)
|
|
169
200
|
value = percent.round
|
|
170
201
|
color = if value >= 85
|
data/lib/kward/cli/rendering.rb
CHANGED
|
@@ -318,7 +318,10 @@ module Kward
|
|
|
318
318
|
end
|
|
319
319
|
|
|
320
320
|
def tool_summary_display_text(summary)
|
|
321
|
-
summary.to_s
|
|
321
|
+
text = summary.to_s
|
|
322
|
+
return text if text.start_with?("read_skill:\n")
|
|
323
|
+
|
|
324
|
+
text.sub("\n", "\n\n")
|
|
322
325
|
end
|
|
323
326
|
|
|
324
327
|
def start_stream_block(label)
|