kward 0.71.0 → 0.72.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 +41 -1
- 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 +394 -0
- data/doc/extensibility.md +16 -7
- data/doc/files.md +100 -0
- data/doc/getting-started.md +25 -18
- data/doc/git.md +122 -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 +74 -4
- data/doc/session-management.md +35 -1
- data/doc/shell.md +286 -0
- data/doc/tabs.md +122 -0
- data/doc/troubleshooting.md +77 -1
- data/doc/usage.md +53 -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/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/runtime_helpers.rb +133 -3
- data/lib/kward/cli/sessions.rb +2 -2
- data/lib/kward/cli/settings.rb +218 -9
- data/lib/kward/cli/slash_commands.rb +415 -2
- data/lib/kward/cli/tabs.rb +695 -0
- data/lib/kward/cli.rb +158 -26
- data/lib/kward/config_files.rb +123 -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 +362 -0
- data/lib/kward/plugin_registry.rb +61 -0
- data/lib/kward/project_files.rb +52 -0
- data/lib/kward/prompt_history.rb +82 -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 +509 -0
- data/lib/kward/prompt_interface/editor/buffer.rb +109 -0
- data/lib/kward/prompt_interface/editor/controller.rb +1018 -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 +353 -0
- data/lib/kward/prompt_interface/editor/modes/vibe.rb +1962 -0
- data/lib/kward/prompt_interface/editor/renderer.rb +243 -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 +1249 -0
- data/lib/kward/prompt_interface/editor/status_text.rb +23 -0
- data/lib/kward/prompt_interface/editor/syntax_highlighter.rb +420 -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 +299 -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 +387 -35
- 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 +98 -50
- data/lib/kward/prompt_interface/runtime_state.rb +43 -0
- data/lib/kward/prompt_interface/screen.rb +16 -0
- data/lib/kward/prompt_interface/selection_prompt.rb +7 -13
- 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 +286 -8
- data/lib/kward/prompts/commands.rb +5 -0
- data/lib/kward/prompts.rb +2 -0
- data/lib/kward/rpc/server.rb +42 -3
- data/lib/kward/rpc/session_manager.rb +35 -47
- data/lib/kward/rpc/session_tree_rows.rb +9 -115
- data/lib/kward/rpc/tool_event_normalizer.rb +1 -1
- 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/text_boundary.rb +25 -0
- data/lib/kward/tools/context_budget_stats.rb +54 -0
- data/lib/kward/tools/context_for_task.rb +202 -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 +68 -0
- data/lib/kward/workers/live_view.rb +49 -0
- data/lib/kward/workers/manager.rb +288 -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 +7 -0
- data/lib/kward/workspace.rb +110 -24
- data/templates/default/fulldoc/html/css/kward.css +107 -36
- data/templates/default/kward_navigation.rb +12 -1
- data/templates/default/layout/html/layout.erb +4 -2
- data/templates/default/layout/html/setup.rb +6 -0
- metadata +53 -1
data/lib/kward/cli/commands.rb
CHANGED
|
@@ -48,10 +48,12 @@ module Kward
|
|
|
48
48
|
#{heading.call("Usage")}
|
|
49
49
|
#{command.call("kward")} Start an interactive chat
|
|
50
50
|
#{command.call("kward")} #{option.call('"Explain this project"')} Run a one-shot prompt
|
|
51
|
+
#{command.call("kward --filter")} #{option.call('"Translate"')} Filter stdin with an instruction
|
|
51
52
|
#{command.call("kward login")} Sign in or save provider credentials
|
|
52
53
|
#{command.call("kward auth status")} Show saved credential status
|
|
53
54
|
#{command.call("kward init")} Install starter prompts and PRINCIPLES.md
|
|
54
55
|
#{command.call("kward doctor")} Check local Kward setup
|
|
56
|
+
#{command.call("kward edit")} #{option.call("<filename>")} Open a file in the integrated editor
|
|
55
57
|
#{command.call("kward sysprompt")} Inspect the effective system prompt
|
|
56
58
|
#{command.call("kward openrouter refresh")} Refresh cached OpenRouter models
|
|
57
59
|
#{command.call("kward pan")} Start Pan mode web UI
|
|
@@ -64,6 +66,7 @@ module Kward
|
|
|
64
66
|
#{command.call("auth status|logout")} Show or clear saved credentials
|
|
65
67
|
#{command.call("init")} Install starter prompts and PRINCIPLES.md
|
|
66
68
|
#{command.call("doctor")} Check local Kward setup
|
|
69
|
+
#{command.call("edit")} #{option.call("<filename>")} Open a file in the integrated editor
|
|
67
70
|
#{command.call("sysprompt")} [--raw] Inspect the effective system prompt
|
|
68
71
|
#{command.call("stats tokens")} [range] [options] Export local token telemetry as CSV
|
|
69
72
|
#{command.call("openrouter refresh|list")} Refresh or list cached OpenRouter models
|
|
@@ -72,14 +75,18 @@ module Kward
|
|
|
72
75
|
|
|
73
76
|
#{heading.call("Options")}
|
|
74
77
|
#{option.call("--working-directory=PATH")} Run Kward from PATH
|
|
78
|
+
#{option.call("--mode=MODE")} Execution mode: auto, chat, oneshot, filter
|
|
79
|
+
#{option.call("--filter")} Shortcut for --mode filter
|
|
75
80
|
#{option.call("--help")}, #{option.call("-h")} Show this help
|
|
76
81
|
#{option.call("--version")}, #{option.call("-v")} Show the installed version
|
|
77
82
|
|
|
78
83
|
#{heading.call("Examples")}
|
|
79
84
|
#{command.call("kward")}
|
|
80
|
-
#{command.call("kward")} #{option.call('"
|
|
81
|
-
#{command.call("git diff | kward")} #{option.call('"
|
|
85
|
+
#{command.call("kward")} #{option.call('"Explain this project"')}
|
|
86
|
+
#{command.call("git diff | kward")} #{option.call('"Summarize the main changes"')}
|
|
87
|
+
#{command.call("echo Hello | kward --filter")} #{option.call('"Translate to German"')}
|
|
82
88
|
#{command.call("kward login openrouter")}
|
|
89
|
+
#{command.call("kward edit lib/main.rb")}
|
|
83
90
|
#{command.call("kward openrouter refresh")}
|
|
84
91
|
#{command.call("kward stats tokens today --bucket hour")}
|
|
85
92
|
|
|
@@ -119,6 +126,11 @@ module Kward
|
|
|
119
126
|
description: "Check local Kward configuration, workspace, auth hints, and writable directories.",
|
|
120
127
|
examples: ["kward doctor", "kward --working-directory ~/code/project doctor"]
|
|
121
128
|
},
|
|
129
|
+
"edit" => {
|
|
130
|
+
usage: "kward edit <filename>",
|
|
131
|
+
description: "Open a file in the integrated editor.",
|
|
132
|
+
examples: ["kward edit lib/main.rb", "kward edit ~/notes/todo.md"]
|
|
133
|
+
},
|
|
122
134
|
"sysprompt" => {
|
|
123
135
|
usage: "kward sysprompt [--raw]",
|
|
124
136
|
description: "Inspect the effective system prompt for a new conversation in the current workspace.",
|
|
@@ -200,6 +212,17 @@ module Kward
|
|
|
200
212
|
@prompt_delimited = true
|
|
201
213
|
remaining.concat(arguments[(index + 1)..] || [])
|
|
202
214
|
break
|
|
215
|
+
when "--experimental-workers"
|
|
216
|
+
@experimental_workers = true
|
|
217
|
+
when "--filter"
|
|
218
|
+
@requested_mode = "filter"
|
|
219
|
+
when "--mode"
|
|
220
|
+
index += 1
|
|
221
|
+
raise ArgumentError, "Missing value for --mode" if index >= arguments.length
|
|
222
|
+
|
|
223
|
+
@requested_mode = normalized_execution_mode(arguments[index])
|
|
224
|
+
when /\A--mode=(.*)\z/
|
|
225
|
+
@requested_mode = normalized_execution_mode(Regexp.last_match(1))
|
|
203
226
|
when "--working-directory"
|
|
204
227
|
index += 1
|
|
205
228
|
raise ArgumentError, "Missing value for --working-directory" if index >= arguments.length
|
|
@@ -215,6 +238,14 @@ module Kward
|
|
|
215
238
|
remaining
|
|
216
239
|
end
|
|
217
240
|
|
|
241
|
+
def normalized_execution_mode(value)
|
|
242
|
+
mode = value.to_s.strip.downcase
|
|
243
|
+
modes = ["auto", "chat", "oneshot", "filter"]
|
|
244
|
+
raise ArgumentError, "Unknown mode: #{value}. Expected one of: #{modes.join(", ")}" unless modes.include?(mode)
|
|
245
|
+
|
|
246
|
+
mode
|
|
247
|
+
end
|
|
248
|
+
|
|
218
249
|
def expanded_working_directory(path)
|
|
219
250
|
value = path.to_s.strip
|
|
220
251
|
raise ArgumentError, "Missing value for --working-directory" if value.empty?
|
|
@@ -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| command[:name] == "workers" }
|
|
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
|