kward 0.83.0 → 0.85.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 +102 -15
- data/CONTRIBUTING.md +74 -0
- data/Gemfile.lock +8 -2
- data/README.md +21 -1
- data/Rakefile +46 -2
- data/SECURITY.md +31 -0
- data/doc/agent-tools.md +13 -1
- data/doc/api.md +21 -2
- data/doc/composer.md +2 -2
- data/doc/configuration.md +95 -25
- data/doc/editor.md +28 -13
- data/doc/extensibility.md +2 -1
- data/doc/files.md +8 -4
- data/doc/getting-started.md +3 -0
- data/doc/git.md +3 -1
- data/doc/pan.md +25 -15
- data/doc/permissions.md +4 -4
- data/doc/platform-support.md +48 -0
- data/doc/plugins.md +464 -15
- data/doc/rpc.md +154 -16
- data/doc/sandboxing.md +11 -5
- data/doc/security.md +10 -3
- data/doc/session-management.md +5 -4
- data/doc/shell.md +62 -45
- data/doc/tabs.md +6 -2
- data/doc/transports.md +15 -0
- data/doc/troubleshooting.md +12 -2
- data/doc/usage.md +9 -6
- data/doc/workspace-tools.md +9 -0
- data/examples/plugins/space_invaders.rb +1 -1
- data/examples/plugins/stardate_footer.rb +2 -2
- data/examples/plugins/telegram/plugin.rb +1 -1
- data/kward.gemspec +5 -4
- data/lib/kward/agent.rb +30 -14
- data/lib/kward/cli/auth_commands.rb +34 -13
- data/lib/kward/cli/commands.rb +83 -62
- data/lib/kward/cli/compaction.rb +9 -3
- data/lib/kward/cli/doctor.rb +39 -17
- data/lib/kward/cli/hook_commands.rb +22 -12
- data/lib/kward/cli/interactive_turn.rb +48 -7
- data/lib/kward/cli/plugins.rb +81 -12
- data/lib/kward/cli/project_skills_commands.rb +8 -4
- data/lib/kward/cli/prompt_interface.rb +52 -5
- data/lib/kward/cli/rendering.rb +18 -9
- data/lib/kward/cli/runtime_helpers.rb +228 -71
- data/lib/kward/cli/sessions.rb +9 -5
- data/lib/kward/cli/settings/menus.rb +745 -0
- data/lib/kward/cli/settings/model.rb +327 -0
- data/lib/kward/cli/settings.rb +6 -1055
- data/lib/kward/cli/slash_commands.rb +56 -17
- data/lib/kward/cli/tabs.rb +244 -33
- data/lib/kward/cli/tool_summaries.rb +14 -0
- data/lib/kward/{cli_transcript_formatter.rb → cli/transcript_formatter.rb} +14 -7
- data/lib/kward/cli/worktrees.rb +65 -2
- data/lib/kward/cli.rb +70 -30
- data/lib/kward/compactor.rb +18 -7
- data/lib/kward/config/core.rb +389 -0
- data/lib/kward/config/extensions.rb +96 -0
- data/lib/kward/config/prompts.rb +313 -0
- data/lib/kward/config/settings.rb +250 -0
- data/lib/kward/config_files.rb +14 -994
- data/lib/kward/conversation.rb +31 -2
- data/lib/kward/image_attachments.rb +1 -1
- data/lib/kward/model/client.rb +36 -24
- data/lib/kward/model/copilot_models.rb +2 -2
- data/lib/kward/model/model_info.rb +20 -3
- data/lib/kward/{openrouter_model_cache.rb → model/openrouter_model_cache.rb} +3 -3
- data/lib/kward/model/payloads.rb +12 -3
- data/lib/kward/model/provider_catalog.rb +5 -0
- data/lib/kward/model/stream_parser.rb +20 -4
- data/lib/kward/model/typesafe_client.rb +78 -0
- data/lib/kward/pan/index.html.erb +3 -3
- data/lib/kward/pan/server.rb +33 -10
- data/lib/kward/permissions/policy.rb +6 -2
- data/lib/kward/plugin_registry.rb +2 -659
- data/lib/kward/plugins/actions.rb +453 -0
- data/lib/kward/plugins/chat_contract.rb +121 -0
- data/lib/kward/{plugin_chat_runtime.rb → plugins/chat_runtime.rb} +62 -18
- data/lib/kward/plugins/host.rb +232 -0
- data/lib/kward/plugins/registry.rb +1190 -0
- data/lib/kward/plugins/resources.rb +206 -0
- data/lib/kward/plugins/turn_request.rb +36 -0
- data/lib/kward/plugins/ui.rb +219 -0
- data/lib/kward/prompt_interface/composer_renderer.rb +44 -40
- data/lib/kward/prompt_interface/composer_state.rb +33 -24
- data/lib/kward/prompt_interface/editor/auto_indent.rb +24 -22
- data/lib/kward/prompt_interface/editor/controller.rb +30 -33
- data/lib/kward/prompt_interface/editor/endwise.rb +13 -4
- data/lib/kward/prompt_interface/editor/markdown_code_block.rb +136 -0
- data/lib/kward/prompt_interface/editor/modes/vibe.rb +289 -44
- data/lib/kward/prompt_interface/editor/renderer.rb +108 -6
- data/lib/kward/prompt_interface/editor/runner.rb +362 -0
- data/lib/kward/prompt_interface/editor/runner_state.rb +78 -0
- data/lib/kward/prompt_interface/editor/scratchpad_languages.rb +74 -0
- data/lib/kward/prompt_interface/editor/scratchpad_runner.rb +182 -0
- data/lib/kward/prompt_interface/editor/state.rb +11 -11
- data/lib/kward/prompt_interface/editor/syntax_highlighter.rb +68 -6
- data/lib/kward/prompt_interface/editor/vibe_state.rb +3 -3
- data/lib/kward/prompt_interface/file_overlay.rb +71 -15
- data/lib/kward/prompt_interface/key_handler.rb +67 -0
- data/lib/kward/prompt_interface/layout.rb +1 -1
- data/lib/kward/prompt_interface/overlay_renderer.rb +7 -5
- data/lib/kward/prompt_interface/plugin_ui_requests.rb +82 -0
- data/lib/kward/prompt_interface/project_browser.rb +415 -14
- data/lib/kward/prompt_interface/runtime_state.rb +56 -2
- data/lib/kward/prompt_interface/screen.rb +11 -4
- data/lib/kward/prompt_interface/selection_prompt.rb +3 -1
- data/lib/kward/prompt_interface/slash_overlay.rb +19 -4
- data/lib/kward/prompt_interface/transcript_renderer.rb +12 -7
- data/lib/kward/prompt_interface.rb +151 -27
- data/lib/kward/prompts/commands.rb +3 -2
- data/lib/kward/prompts.rb +1 -1
- data/lib/kward/{adaptive_pty_output_sink.rb → pty/adaptive_output_sink.rb} +1 -1
- data/lib/kward/pty/detached_run.rb +44 -0
- data/lib/kward/{interactive_pty_runner.rb → pty/interactive_runner.rb} +104 -30
- data/lib/kward/{local_command_runner.rb → pty/local_command_runner.rb} +1 -1
- data/lib/kward/{local_pty_command_runner.rb → pty/local_pty_runner.rb} +3 -9
- data/lib/kward/{pty_output_sink.rb → pty/output_sink.rb} +52 -0
- data/lib/kward/{pty_transcript_normalizer.rb → pty/transcript_normalizer.rb} +1 -1
- data/lib/kward/rpc/plugin_chat_manager.rb +30 -10
- data/lib/kward/rpc/prompt_bridge.rb +25 -0
- data/lib/kward/rpc/server.rb +91 -12
- data/lib/kward/rpc/session_manager.rb +147 -44
- data/lib/kward/rpc/session_tree_rows.rb +2 -2
- data/lib/kward/rpc/tool_metadata.rb +1 -1
- data/lib/kward/rpc/transcript_normalizer.rb +7 -3
- data/lib/kward/sandbox/command_runner.rb +1 -1
- data/lib/kward/{session_catalog.rb → sessions/catalog.rb} +1 -1
- data/lib/kward/{session_store.rb → sessions/store.rb} +9 -9
- data/lib/kward/{session_tree_nodes.rb → sessions/tree_nodes.rb} +3 -3
- data/lib/kward/{session_tree_renderer.rb → sessions/tree_renderer.rb} +4 -4
- data/lib/kward/{session_tree_tool_display.rb → sessions/tree_tool_display.rb} +1 -1
- data/lib/kward/{ekwsh.rb → shell/kwsh.rb} +38 -19
- data/lib/kward/shell/kwshrc.rb +233 -0
- data/lib/kward/{persistent_shell_session.rb → shell/persistent_session.rb} +121 -28
- data/lib/kward/{shell_prompt.rb → shell/prompt.rb} +2 -0
- data/lib/kward/{shell_prompt_session.rb → shell/prompt_session.rb} +1 -1
- data/lib/kward/skills/trust_store.rb +1 -1
- data/lib/kward/tabs/driver.rb +194 -0
- data/lib/kward/{tab_store.rb → tabs/store.rb} +2 -2
- data/lib/kward/{ansi.rb → terminal/ansi.rb} +110 -10
- data/lib/kward/{clipboard.rb → terminal/clipboard.rb} +1 -1
- data/lib/kward/{terminal_image_support.rb → terminal/image_support.rb} +1 -1
- data/lib/kward/{terminal_keys.rb → terminal/keys.rb} +12 -0
- data/lib/kward/terminal/text.rb +121 -0
- data/lib/kward/text_matcher.rb +18 -0
- data/lib/kward/tools/base.rb +18 -0
- data/lib/kward/tools/context_for_task.rb +15 -6
- data/lib/kward/tools/edit_file.rb +9 -6
- data/lib/kward/tools/git_commit.rb +13 -7
- data/lib/kward/tools/list_directory.rb +4 -4
- data/lib/kward/tools/open_editor.rb +41 -0
- data/lib/kward/tools/plugin_tool.rb +41 -0
- data/lib/kward/tools/prepare_shell_command.rb +1 -1
- data/lib/kward/tools/read_file.rb +7 -6
- data/lib/kward/tools/registry.rb +109 -17
- data/lib/kward/tools/run_shell_command.rb +10 -8
- data/lib/kward/tools/search/code.rb +1 -1
- data/lib/kward/tools/summarize_file_structure.rb +5 -5
- data/lib/kward/tools/tool_call.rb +3 -1
- data/lib/kward/tools/typesafe_evaluate.rb +81 -0
- data/lib/kward/tools/workspace_targets.rb +58 -0
- data/lib/kward/tools/write_file.rb +9 -6
- data/lib/kward/{export_path.rb → transcripts/export_path.rb} +1 -1
- data/lib/kward/{markdown_transcript.rb → transcripts/markdown_transcript.rb} +2 -2
- data/lib/kward/transport/contracts.rb +200 -0
- data/lib/kward/transport/gateway.rb +79 -35
- data/lib/kward/transport/plugin_chat_gateway.rb +3 -2
- data/lib/kward/transport.rb +1 -200
- data/lib/kward/version.rb +1 -1
- data/lib/kward/{workspace_factory.rb → workspace/factory.rb} +2 -2
- data/lib/kward/{project_files.rb → workspace/files.rb} +2 -2
- data/lib/kward/{git_worktree_manager.rb → workspace/git_worktree_manager.rb} +28 -0
- data/lib/kward/{workspace.rb → workspace/workspace.rb} +3 -3
- data/templates/default/kward_navigation.rb +1 -0
- data/templates/default/layout/html/footer.erb +10 -0
- data/templates/default/layout/html/headers.erb +23 -0
- data/templates/default/layout/html/layout.erb +2 -2
- data/templates/default/layout/html/setup.rb +41 -2
- metadata +94 -47
- data/lib/kward/scratchpad_runner.rb +0 -56
- data/lib/kward/tab_driver.rb +0 -90
- /data/lib/kward/{editor_prompt.rb → cli/editor_prompt.rb} +0 -0
- /data/lib/kward/{editor_prompt_session.rb → cli/editor_prompt_session.rb} +0 -0
- /data/lib/kward/{diff_view_mode.rb → prompt_interface/editor/diff_view_mode.rb} +0 -0
- /data/lib/kward/{editor_mode.rb → prompt_interface/editor/editor_mode.rb} +0 -0
- /data/lib/kward/{session_diff.rb → sessions/diff.rb} +0 -0
- /data/lib/kward/{session_naming.rb → sessions/naming.rb} +0 -0
- /data/lib/kward/{session_trash.rb → sessions/trash.rb} +0 -0
- /data/lib/kward/{terminal_sequences.rb → terminal/sequences.rb} +0 -0
- /data/lib/kward/{transcript_export.rb → transcripts/transcript_export.rb} +0 -0
- /data/lib/kward/{path_guard.rb → workspace/path_guard.rb} +0 -0
data/lib/kward/cli/doctor.rb
CHANGED
|
@@ -8,11 +8,32 @@ module Kward
|
|
|
8
8
|
|
|
9
9
|
# Writes the doctor output for the terminal CLI flow.
|
|
10
10
|
def print_doctor
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
checks = doctor_checks
|
|
12
|
+
core_checks, optional_checks = checks.partition { |check| !check[:optional] }
|
|
13
|
+
lines = [colored("Kward Doctor", :green, :bold), "", colored("Core checks", :blue, :bold)]
|
|
14
|
+
lines.concat(doctor_check_lines(core_checks))
|
|
15
|
+
if optional_checks.any?
|
|
16
|
+
lines << ""
|
|
17
|
+
lines << colored("Optional", :blue, :bold)
|
|
18
|
+
lines.concat(doctor_check_lines(optional_checks))
|
|
14
19
|
end
|
|
20
|
+
lines << ""
|
|
21
|
+
lines << doctor_summary(checks)
|
|
15
22
|
@prompt.say lines.join("\n")
|
|
23
|
+
checks.none? { |check| check.fetch(:status) == :error && !check[:optional] }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def doctor_check_lines(checks)
|
|
27
|
+
checks.map { |check| "#{doctor_mark(check.fetch(:status))} #{check.fetch(:label)}: #{check.fetch(:message)}" }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def doctor_summary(checks)
|
|
31
|
+
errors = checks.count { |check| check.fetch(:status) == :error && !check[:optional] }
|
|
32
|
+
warnings = checks.count { |check| check.fetch(:status) == :warning && !check[:optional] }
|
|
33
|
+
return "Kward needs attention: #{errors} core check#{errors == 1 ? "" : "s"} failed." if errors.positive?
|
|
34
|
+
return "Kward is ready with #{warnings} warning#{warnings == 1 ? "" : "s"}." if warnings.positive?
|
|
35
|
+
|
|
36
|
+
"Kward is ready."
|
|
16
37
|
end
|
|
17
38
|
|
|
18
39
|
def doctor_checks
|
|
@@ -108,29 +129,28 @@ module Kward
|
|
|
108
129
|
{ status: :error, label: "Local endpoint", message: "invalid URL: #{url}" }
|
|
109
130
|
end
|
|
110
131
|
|
|
111
|
-
def doctor_auth_check(
|
|
112
|
-
openai_auth = OpenAIOAuth.default_auth_path
|
|
113
|
-
github_auth = GithubOAuth.default_auth_path
|
|
114
|
-
has_openrouter = !config.to_h["openrouter_api_key"].to_s.empty? || !ENV["OPENROUTER_API_KEY"].to_s.empty?
|
|
132
|
+
def doctor_auth_check(_config)
|
|
115
133
|
local_provider = @client.respond_to?(:current_provider) && @client.current_provider == "Local"
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
paths << "OpenRouter API key" if has_openrouter
|
|
120
|
-
paths << "Local endpoint (no authentication required)" if local_provider
|
|
121
|
-
return { status: :ok, label: "Auth", message: paths.join(", ") } if paths.any?
|
|
134
|
+
configured = auth_credentials.select { |credential| credential.fetch(:configured) }.map { |credential| credential.fetch(:label) }
|
|
135
|
+
configured << "Local endpoint (no authentication required)" if local_provider
|
|
136
|
+
return { status: :ok, label: "Auth", message: configured.join(", ") } if configured.any?
|
|
122
137
|
|
|
123
138
|
{ status: :warning, label: "Auth", message: "no saved credentials found; run `kward login`" }
|
|
139
|
+
rescue ConfigFiles::ConfigError
|
|
140
|
+
{ status: :warning, label: "Auth", message: "skipped because config is invalid" }
|
|
124
141
|
end
|
|
125
142
|
|
|
126
143
|
def doctor_pan_check(config_result)
|
|
127
|
-
return { status: :
|
|
144
|
+
return { status: :optional, label: "Pan mode", message: "skipped because config is invalid", optional: true } if config_result.is_a?(ConfigFiles::ConfigError)
|
|
128
145
|
|
|
129
146
|
pan = config_result.to_h["pan_mode"] || {}
|
|
130
|
-
|
|
131
|
-
|
|
147
|
+
environment_password = ENV["KWARD_PAN_PASSWORD"].to_s
|
|
148
|
+
password = environment_password.empty? ? pan["password"].to_s : environment_password
|
|
149
|
+
if !pan["username"].to_s.empty? && !password.empty?
|
|
150
|
+
source = environment_password.empty? ? "config" : "environment"
|
|
151
|
+
{ status: :ok, label: "Pan mode", message: "credentials configured (password from #{source})", optional: true }
|
|
132
152
|
else
|
|
133
|
-
{ status: :
|
|
153
|
+
{ status: :optional, label: "Pan mode", message: "not configured", optional: true }
|
|
134
154
|
end
|
|
135
155
|
end
|
|
136
156
|
|
|
@@ -140,6 +160,8 @@ module Kward
|
|
|
140
160
|
colored("✓", :green, :bold)
|
|
141
161
|
when :warning
|
|
142
162
|
colored("!", :yellow, :bold)
|
|
163
|
+
when :optional
|
|
164
|
+
colored("•", :gray, :bold)
|
|
143
165
|
else
|
|
144
166
|
colored("✗", :red, :bold)
|
|
145
167
|
end
|
|
@@ -9,7 +9,9 @@ module Kward
|
|
|
9
9
|
module HookCommands
|
|
10
10
|
private
|
|
11
11
|
|
|
12
|
-
def handle_hooks_command(argument)
|
|
12
|
+
def handle_hooks_command(argument, command_line: false)
|
|
13
|
+
previous_output_mode = @hook_command_line_output
|
|
14
|
+
@hook_command_line_output = command_line
|
|
13
15
|
subcommand, rest = argument.to_s.strip.split(/\s+/, 2)
|
|
14
16
|
subcommand = "list" if subcommand.to_s.empty?
|
|
15
17
|
|
|
@@ -27,8 +29,16 @@ module Kward
|
|
|
27
29
|
when "untrust"
|
|
28
30
|
untrust_workspace_hooks
|
|
29
31
|
else
|
|
30
|
-
|
|
32
|
+
raise ArgumentError, command_usage("hooks") if command_line
|
|
33
|
+
|
|
34
|
+
hook_command_output("Usage: /hooks [list|events|logs|doctor|trust|untrust]")
|
|
31
35
|
end
|
|
36
|
+
ensure
|
|
37
|
+
@hook_command_line_output = previous_output_mode
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def hook_command_output(text)
|
|
41
|
+
@hook_command_line_output ? @prompt.say(text) : runtime_output(text)
|
|
32
42
|
end
|
|
33
43
|
|
|
34
44
|
def print_hooks_list
|
|
@@ -46,7 +56,7 @@ module Kward
|
|
|
46
56
|
lines << "- #{details.join(' ')}"
|
|
47
57
|
end
|
|
48
58
|
end
|
|
49
|
-
|
|
59
|
+
hook_command_output(lines.join("\n"))
|
|
50
60
|
end
|
|
51
61
|
|
|
52
62
|
def print_hooks_events
|
|
@@ -57,7 +67,7 @@ module Kward
|
|
|
57
67
|
suffix = fields.empty? ? "" : " modifies=#{fields.join(',')}"
|
|
58
68
|
lines << "- #{event_name} failure_policy=#{Hooks::Catalog.failure_policy(event_name)}#{suffix}"
|
|
59
69
|
end
|
|
60
|
-
|
|
70
|
+
hook_command_output(lines.join("\n"))
|
|
61
71
|
end
|
|
62
72
|
|
|
63
73
|
def print_hooks_logs(argument)
|
|
@@ -65,7 +75,7 @@ module Kward
|
|
|
65
75
|
count = 20 unless count.positive?
|
|
66
76
|
path = hooks_log_path
|
|
67
77
|
unless File.file?(path)
|
|
68
|
-
|
|
78
|
+
hook_command_output("No lifecycle hook audit log found at #{path}.")
|
|
69
79
|
return
|
|
70
80
|
end
|
|
71
81
|
|
|
@@ -75,7 +85,7 @@ module Kward
|
|
|
75
85
|
nil
|
|
76
86
|
end
|
|
77
87
|
if records.empty?
|
|
78
|
-
|
|
88
|
+
hook_command_output("No readable lifecycle hook audit records found at #{path}.")
|
|
79
89
|
return
|
|
80
90
|
end
|
|
81
91
|
|
|
@@ -83,7 +93,7 @@ module Kward
|
|
|
83
93
|
records.each do |record|
|
|
84
94
|
lines << format_hook_log_record(record)
|
|
85
95
|
end
|
|
86
|
-
|
|
96
|
+
hook_command_output(lines.join("\n"))
|
|
87
97
|
end
|
|
88
98
|
|
|
89
99
|
def print_hooks_doctor
|
|
@@ -105,7 +115,7 @@ module Kward
|
|
|
105
115
|
rescue StandardError => e
|
|
106
116
|
lines << "Error: #{e.message}"
|
|
107
117
|
end
|
|
108
|
-
|
|
118
|
+
hook_command_output(lines.join("\n"))
|
|
109
119
|
end
|
|
110
120
|
|
|
111
121
|
def configured_hook_entries
|
|
@@ -226,16 +236,16 @@ module Kward
|
|
|
226
236
|
|
|
227
237
|
def trust_workspace_hooks
|
|
228
238
|
ConfigFiles.trust_workspace_hooks!(current_workspace_root)
|
|
229
|
-
|
|
239
|
+
hook_command_output("Trusted workspace hooks: #{ConfigFiles.workspace_hooks_path(current_workspace_root)}")
|
|
230
240
|
rescue StandardError => e
|
|
231
|
-
|
|
241
|
+
hook_command_output("Workspace hook trust error: #{e.message}")
|
|
232
242
|
end
|
|
233
243
|
|
|
234
244
|
def untrust_workspace_hooks
|
|
235
245
|
ConfigFiles.untrust_workspace_hooks!(current_workspace_root)
|
|
236
|
-
|
|
246
|
+
hook_command_output("Untrusted workspace hooks: #{ConfigFiles.workspace_hooks_path(current_workspace_root)}")
|
|
237
247
|
rescue StandardError => e
|
|
238
|
-
|
|
248
|
+
hook_command_output("Workspace hook trust error: #{e.message}")
|
|
239
249
|
end
|
|
240
250
|
|
|
241
251
|
def hooks_log_path
|
|
@@ -47,9 +47,10 @@ module Kward
|
|
|
47
47
|
def build_editor_prompt_agent(active_agent, context, registry)
|
|
48
48
|
active_conversation = active_agent.conversation
|
|
49
49
|
config = safely_read_config.to_h
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
configured_provider = ConfigFiles.editor_agent_provider(config)
|
|
51
|
+
provider = configured_provider || active_conversation.provider || current_model_provider
|
|
52
|
+
model = ConfigFiles.editor_agent_model(config) || (configured_provider ? nil : active_conversation.model || current_model_id)
|
|
53
|
+
reasoning = ConfigFiles.editor_agent_reasoning_effort(config) || (configured_provider ? nil : active_conversation.reasoning_effort || current_reasoning_effort)
|
|
53
54
|
conversation = Conversation.new(
|
|
54
55
|
system_message: EditorPrompt.system_message,
|
|
55
56
|
workspace_root: context[:workspace_root] || active_conversation.workspace_root,
|
|
@@ -61,6 +62,7 @@ module Kward
|
|
|
61
62
|
client: @client,
|
|
62
63
|
tool_registry: registry,
|
|
63
64
|
conversation: conversation,
|
|
65
|
+
strict_provider: !configured_provider.nil?,
|
|
64
66
|
warning_sink: ConfigFiles.warning_sink
|
|
65
67
|
)
|
|
66
68
|
end
|
|
@@ -76,9 +78,11 @@ module Kward
|
|
|
76
78
|
event_queue = Queue.new
|
|
77
79
|
stream_state = {
|
|
78
80
|
streamed: false,
|
|
81
|
+
assistant_arrived: false,
|
|
79
82
|
last_flush: monotonic_now,
|
|
80
83
|
stream_block_open: false,
|
|
81
84
|
markdown_streams: {},
|
|
85
|
+
tool_elapsed_ms: {},
|
|
82
86
|
defer_assistant_streaming: defer_assistant_streaming?(agent)
|
|
83
87
|
}
|
|
84
88
|
markdown_chunks = []
|
|
@@ -126,19 +130,29 @@ module Kward
|
|
|
126
130
|
if busy_replacement_agent? || suppress_transcript
|
|
127
131
|
discard_interactive_events(event_queue, markdown_chunks, stream_state, force: true)
|
|
128
132
|
else
|
|
129
|
-
|
|
133
|
+
drain_remaining_interactive_events(event_queue, markdown_chunks, stream_state, agent)
|
|
130
134
|
end
|
|
131
135
|
completed = !cancelled && !busy_replacement_agent? && !error
|
|
132
136
|
on_complete&.call(successful: completed, cancelled: cancelled, error: error, answer: answer)
|
|
133
137
|
raise error if error && !error.is_a?(Cancellation::CancelledError) && !busy_replacement_agent?
|
|
134
138
|
|
|
135
139
|
@prompt.commit_editor_prompt(editor_prompt_session) if editor_prompt_session && completed
|
|
136
|
-
@prompt.say("\n#{colored(
|
|
140
|
+
@prompt.say("\n#{colored(transcript_marker("Assistant"), :activity, :bold)} #{render_markdown_transcript(answer)}\n") unless suppress_transcript || cancelled || busy_replacement_agent? || stream_state[:streamed] || answer.to_s.empty?
|
|
137
141
|
persist_memory_state(agent.conversation) if agent.respond_to?(:conversation) && !suppress_transcript
|
|
138
142
|
auto_summarize_memory(agent.conversation) if agent.respond_to?(:conversation) && !suppress_transcript && queued_inputs.empty? && !cancelled
|
|
139
143
|
queued_inputs
|
|
140
144
|
ensure
|
|
141
145
|
@prompt.finish_busy_input if @prompt.respond_to?(:finish_busy_input)
|
|
146
|
+
if @prompt.respond_to?(:show_completion) && !busy_replacement_agent?
|
|
147
|
+
completion = if cancelled
|
|
148
|
+
:cancelled
|
|
149
|
+
elsif error
|
|
150
|
+
:failed
|
|
151
|
+
elsif completed
|
|
152
|
+
:success
|
|
153
|
+
end
|
|
154
|
+
@prompt.show_completion(completion) if completion
|
|
155
|
+
end
|
|
142
156
|
if editor_prompt_session && @prompt.respond_to?(:resume_editor_for_agent)
|
|
143
157
|
status = if cancelled
|
|
144
158
|
"Agent request cancelled"
|
|
@@ -174,6 +188,14 @@ module Kward
|
|
|
174
188
|
flush_interactive_markdown_deltas(markdown_chunks, stream_state, force: force)
|
|
175
189
|
end
|
|
176
190
|
|
|
191
|
+
def drain_remaining_interactive_events(event_queue, markdown_chunks, stream_state, agent)
|
|
192
|
+
until event_queue.empty?
|
|
193
|
+
drain_interactive_events(event_queue, markdown_chunks, stream_state, agent)
|
|
194
|
+
sleep STREAM_RENDER_INTERVAL unless event_queue.empty?
|
|
195
|
+
end
|
|
196
|
+
drain_interactive_events(event_queue, markdown_chunks, stream_state, agent, force: true)
|
|
197
|
+
end
|
|
198
|
+
|
|
177
199
|
def discard_interactive_events(event_queue, markdown_chunks, stream_state, force: false)
|
|
178
200
|
drained = 0
|
|
179
201
|
loop do
|
|
@@ -193,12 +215,19 @@ module Kward
|
|
|
193
215
|
case event
|
|
194
216
|
when Events::ReasoningDelta
|
|
195
217
|
stream_state[:streamed] = true
|
|
218
|
+
update_busy_activity("reasoning")
|
|
196
219
|
append_markdown_delta(markdown_chunks, "Reasoning", event.delta)
|
|
197
220
|
when Events::ReasoningBoundary
|
|
198
221
|
stream_state[:streamed] = true
|
|
199
222
|
finish_interactive_markdown_deltas(markdown_chunks, stream_state)
|
|
223
|
+
update_busy_activity("thinking")
|
|
200
224
|
when Events::AssistantDelta
|
|
201
225
|
stream_state[:streamed] = true
|
|
226
|
+
unless stream_state[:assistant_arrived]
|
|
227
|
+
@prompt.show_response_arrival if @prompt.respond_to?(:show_response_arrival)
|
|
228
|
+
stream_state[:assistant_arrived] = true
|
|
229
|
+
end
|
|
230
|
+
update_busy_activity("responding")
|
|
202
231
|
append_markdown_delta(markdown_chunks, "Assistant", event.delta)
|
|
203
232
|
when Events::Steering
|
|
204
233
|
finish_interactive_markdown_deltas(markdown_chunks, stream_state)
|
|
@@ -208,18 +237,30 @@ module Kward
|
|
|
208
237
|
when Events::Retry
|
|
209
238
|
stream_state[:streamed] = true
|
|
210
239
|
finish_interactive_markdown_deltas(markdown_chunks, stream_state)
|
|
240
|
+
update_busy_activity("retrying")
|
|
211
241
|
print_retry(event)
|
|
212
242
|
when Events::ToolCall
|
|
213
243
|
stream_state[:streamed] = true
|
|
214
244
|
finish_interactive_markdown_deltas(markdown_chunks, stream_state)
|
|
245
|
+
update_busy_activity(tool_activity(event.tool_call))
|
|
246
|
+
when Events::ToolUpdate
|
|
247
|
+
key = tool_call_id(event.tool_call) || event.tool_call.object_id
|
|
248
|
+
stream_state[:tool_elapsed_ms][key] = event.elapsed_ms
|
|
215
249
|
when Events::ToolResult
|
|
216
250
|
stream_state[:streamed] = true
|
|
217
251
|
finish_interactive_markdown_deltas(markdown_chunks, stream_state)
|
|
218
252
|
update_session_diff(event.content, tool_call: event.tool_call)
|
|
219
|
-
|
|
253
|
+
key = tool_call_id(event.tool_call) || event.tool_call.object_id
|
|
254
|
+
elapsed_ms = stream_state[:tool_elapsed_ms].delete(key)
|
|
255
|
+
print_tool_result(event.tool_call, event.content, line_limit: INTERACTIVE_TOOL_OUTPUT_LINE_LIMIT, elapsed_ms: elapsed_ms)
|
|
256
|
+
update_busy_activity("thinking")
|
|
220
257
|
end
|
|
221
258
|
end
|
|
222
259
|
|
|
260
|
+
def update_busy_activity(activity)
|
|
261
|
+
@prompt.update_busy_activity(activity) if @prompt.respond_to?(:update_busy_activity)
|
|
262
|
+
end
|
|
263
|
+
|
|
223
264
|
def flush_interactive_markdown_deltas(markdown_chunks, stream_state, force: false)
|
|
224
265
|
if force
|
|
225
266
|
finish_interactive_markdown_deltas(markdown_chunks, stream_state)
|
|
@@ -347,7 +388,7 @@ module Kward
|
|
|
347
388
|
streamed = true if !suppress_transcript && render_blocking_turn_event(event, markdown_chunks, tool_line_limit: INTERACTIVE_TOOL_OUTPUT_LINE_LIMIT)
|
|
348
389
|
end
|
|
349
390
|
flush_markdown_deltas(markdown_chunks) if streamed
|
|
350
|
-
@prompt.say("\n#{colored(
|
|
391
|
+
@prompt.say("\n#{colored(transcript_marker("Assistant"), :activity, :bold)} #{render_markdown_transcript(answer)}\n") unless suppress_transcript || streamed || answer.to_s.empty?
|
|
351
392
|
persist_memory_state(agent.conversation) if agent.respond_to?(:conversation) && !suppress_transcript
|
|
352
393
|
auto_summarize_memory(agent.conversation) if agent.respond_to?(:conversation) && !suppress_transcript
|
|
353
394
|
on_complete&.call(successful: true, cancelled: false, error: nil, answer: answer)
|
data/lib/kward/cli/plugins.rb
CHANGED
|
@@ -11,7 +11,7 @@ module Kward
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def plugin_registry
|
|
14
|
-
@plugin_registry ||= PluginRegistry.load(reserved_commands: reserved_slash_command_names)
|
|
14
|
+
@plugin_registry ||= PluginRegistry.load(reserved_commands: reserved_slash_command_names).tap(&:start!)
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
def plugin_commands
|
|
@@ -30,15 +30,22 @@ module Kward
|
|
|
30
30
|
plugin_registry.interactive_command_for(command)
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
-
def reload_plugins(conversation)
|
|
33
|
+
def reload_plugins(conversation, tool_registry: nil)
|
|
34
34
|
@prompt_templates = nil
|
|
35
|
-
|
|
35
|
+
registry = PluginRegistry.load(reserved_commands: reserved_slash_command_names)
|
|
36
|
+
@plugin_registry&.reload!
|
|
37
|
+
@plugin_registry = registry.tap(&:start!)
|
|
36
38
|
@prompt.update_slash_commands(slash_command_entries) if @prompt.respond_to?(:update_slash_commands)
|
|
37
39
|
conversation.plugin_registry = @plugin_registry if conversation.respond_to?(:plugin_registry=)
|
|
38
40
|
conversation.refresh_system_message! if conversation.respond_to?(:refresh_system_message!)
|
|
41
|
+
tool_registry&.replace_plugin_tools!(@plugin_registry.tools)
|
|
39
42
|
runtime_output("Plugins reloaded.")
|
|
40
43
|
end
|
|
41
44
|
|
|
45
|
+
def shutdown_plugins
|
|
46
|
+
@plugin_registry&.shutdown!
|
|
47
|
+
end
|
|
48
|
+
|
|
42
49
|
def lifecycle_hook_manager(conversation)
|
|
43
50
|
manager = Hooks::ConfigLoader.new(ConfigFiles.lifecycle_hooks_config(conversation.workspace_root)).manager
|
|
44
51
|
plugin_registry.hook_handlers.each do |hook|
|
|
@@ -50,7 +57,7 @@ module Kward
|
|
|
50
57
|
end
|
|
51
58
|
|
|
52
59
|
def lifecycle_hook_context(conversation)
|
|
53
|
-
plugin_context(conversation, "")
|
|
60
|
+
plugin_context(conversation, "", requests: false, tool_requests: true)
|
|
54
61
|
end
|
|
55
62
|
|
|
56
63
|
def run_lifecycle_hook(name, conversation:, payload: {}, session: @active_session)
|
|
@@ -81,7 +88,7 @@ module Kward
|
|
|
81
88
|
return [false, nil] unless command
|
|
82
89
|
return [false, nil] unless prompt_interface? && @prompt.respond_to?(:start_interactive)
|
|
83
90
|
|
|
84
|
-
context = plugin_context(agent.conversation, argument)
|
|
91
|
+
context = plugin_context(agent.conversation, argument, requests: false)
|
|
85
92
|
controller = @prompt.start_interactive(title: "/#{name}", rows: command.rows, fps: command.fps)
|
|
86
93
|
command.handler.call(controller, context)
|
|
87
94
|
run_interactive_loop
|
|
@@ -150,26 +157,88 @@ module Kward
|
|
|
150
157
|
PromptCommands.expand(input, templates: prompt_templates, reserved_commands: builtin_slash_command_names)
|
|
151
158
|
end
|
|
152
159
|
|
|
153
|
-
def
|
|
160
|
+
def run_plugin_command_and_turn(name, argument, agent)
|
|
161
|
+
origin_tab = active_tab
|
|
162
|
+
_handled, request = run_busy_local_command_and_requeue(activity: "running") do |cancellation|
|
|
163
|
+
run_plugin_command(name, argument, agent, cancellation: cancellation)
|
|
164
|
+
end
|
|
165
|
+
return [true, nil] unless request
|
|
166
|
+
return [true, nil] if origin_tab && (!@tabs.include?(origin_tab) || !origin_tab.agent.equal?(agent))
|
|
167
|
+
|
|
168
|
+
flush_pending_reasoning_config(conversation: agent.conversation)
|
|
169
|
+
if origin_tab
|
|
170
|
+
session = origin_tab.session
|
|
171
|
+
session.rename(default_session_name(request.to_s)) if session && session.name.to_s.empty?
|
|
172
|
+
submit_tab_input(origin_tab, request, display_input: request.to_s)
|
|
173
|
+
else
|
|
174
|
+
auto_name_active_session(request.to_s)
|
|
175
|
+
pending = run_interactive_turn(agent, request, display_input: request.to_s)
|
|
176
|
+
pending.reverse_each { |input| @pending_inputs.unshift(input) }
|
|
177
|
+
end
|
|
178
|
+
[true, nil]
|
|
179
|
+
rescue Cancellation::CancelledError
|
|
180
|
+
[true, nil]
|
|
181
|
+
rescue StandardError => e
|
|
182
|
+
runtime_output("Error: #{e.message}")
|
|
183
|
+
[true, nil]
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def run_plugin_command(name, argument, agent, cancellation: nil)
|
|
154
187
|
command = plugin_command_for(name)
|
|
155
188
|
return [false, nil] unless command
|
|
156
189
|
|
|
190
|
+
cancellation&.raise_if_cancelled!
|
|
157
191
|
agent.conversation.plugin_registry ||= plugin_registry if agent.conversation.respond_to?(:plugin_registry)
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
192
|
+
arguments = command.parse_arguments(argument)
|
|
193
|
+
context = plugin_context(agent.conversation, arguments, cancellation: cancellation, turn_command: command)
|
|
194
|
+
result = command.normalize_result(command.handler.call(arguments, context))
|
|
195
|
+
cancellation&.raise_if_cancelled!
|
|
196
|
+
runtime_output(result.message) if command.typed? && !result.message.to_s.empty?
|
|
197
|
+
[true, context.requested_turn]
|
|
198
|
+
rescue Cancellation::CancelledError
|
|
199
|
+
raise
|
|
161
200
|
rescue StandardError => e
|
|
162
201
|
runtime_output("Plugin command /#{name} error: #{e.message}")
|
|
163
202
|
[true, nil]
|
|
164
203
|
end
|
|
165
204
|
|
|
166
|
-
def plugin_context(conversation, args)
|
|
205
|
+
def plugin_context(conversation, args, requests: true, tool_requests: nil, cancellation: nil, turn_command: nil)
|
|
206
|
+
say_callback = lambda { |message| runtime_output(message) }
|
|
207
|
+
tool_requests = requests if tool_requests.nil?
|
|
167
208
|
PluginRegistry::Context.new(
|
|
168
209
|
conversation: conversation,
|
|
169
210
|
args: args,
|
|
170
211
|
session: @active_session,
|
|
171
212
|
workspace_root: conversation.workspace_root,
|
|
172
|
-
say_callback:
|
|
213
|
+
say_callback: say_callback,
|
|
214
|
+
cancellation: cancellation,
|
|
215
|
+
ui: plugin_ui(say_callback, requests: requests),
|
|
216
|
+
tool_ui: plugin_ui(say_callback, requests: tool_requests),
|
|
217
|
+
turn_command: turn_command
|
|
218
|
+
)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def plugin_ui(say_callback, requests:)
|
|
222
|
+
capabilities = { progress: true, notify: true }
|
|
223
|
+
requester = nil
|
|
224
|
+
if requests && @prompt.respond_to?(:request_plugin_ui)
|
|
225
|
+
capabilities.merge!(question: true, select: true, confirm: true, input: true)
|
|
226
|
+
requester = lambda do |kind, payload, cancellation: nil|
|
|
227
|
+
@prompt.request_plugin_ui(kind, payload, cancellation: cancellation)
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
emitter = lambda do |kind, payload|
|
|
231
|
+
case kind
|
|
232
|
+
when :notify
|
|
233
|
+
runtime_output(payload[:message])
|
|
234
|
+
when :progress
|
|
235
|
+
text = payload[:percent] ? "#{payload[:message]} (#{payload[:percent]}%)" : payload[:message]
|
|
236
|
+
runtime_output(text)
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
PluginUI.new(
|
|
240
|
+
backend: PluginUI::Backend.new(capabilities: capabilities, requester: requester, emitter: emitter),
|
|
241
|
+
say_callback: say_callback
|
|
173
242
|
)
|
|
174
243
|
end
|
|
175
244
|
|
|
@@ -201,7 +270,7 @@ module Kward
|
|
|
201
270
|
return unless conversation
|
|
202
271
|
return if plugin_registry.transcript_event_handlers.empty?
|
|
203
272
|
|
|
204
|
-
plugin_registry.notify_transcript_event(event, plugin_context(conversation, ""))
|
|
273
|
+
plugin_registry.notify_transcript_event(event, plugin_context(conversation, "", requests: false))
|
|
205
274
|
end
|
|
206
275
|
|
|
207
276
|
def notify_plugin_tab_transcript_event(event, driver)
|
|
@@ -7,20 +7,20 @@ module Kward
|
|
|
7
7
|
argument = Array(arguments).join(" ").strip.downcase
|
|
8
8
|
_workspace_root, candidates, coordinator = project_skill_trust_coordinator
|
|
9
9
|
if candidates.empty? && argument != "untrust"
|
|
10
|
-
|
|
10
|
+
project_skills_cli_output("No project skills found in the current workspace.")
|
|
11
11
|
return
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
case argument
|
|
15
15
|
when "", "status"
|
|
16
16
|
lines = candidates.empty? ? ["No project skills found in the current workspace."] : candidates.map { |candidate| "#{relative_workspace_path(candidate.path)}: #{coordinator.decision(candidate) || "needs review"}" }
|
|
17
|
-
|
|
17
|
+
project_skills_cli_output(lines.join("\n"))
|
|
18
18
|
when "trust"
|
|
19
19
|
coordinator.record!(candidates, "allow")
|
|
20
|
-
|
|
20
|
+
project_skills_cli_output("Project skills trusted for the current skill snapshots.")
|
|
21
21
|
when "untrust"
|
|
22
22
|
coordinator.remove_workspace!
|
|
23
|
-
|
|
23
|
+
project_skills_cli_output("Project skill trust removed for this workspace.")
|
|
24
24
|
when "review"
|
|
25
25
|
review_project_skills(candidates)
|
|
26
26
|
else
|
|
@@ -43,6 +43,10 @@ module Kward
|
|
|
43
43
|
|
|
44
44
|
private
|
|
45
45
|
|
|
46
|
+
def project_skills_cli_output(message)
|
|
47
|
+
@prompt.say("#{colored("Project skills", :green, :bold)}\n\n#{message}")
|
|
48
|
+
end
|
|
49
|
+
|
|
46
50
|
def project_skill_trust_coordinator
|
|
47
51
|
workspace_root = current_workspace_root
|
|
48
52
|
candidates = ConfigFiles.project_skill_candidates(workspace_root: workspace_root)
|
|
@@ -45,6 +45,7 @@ module Kward
|
|
|
45
45
|
editor_line_numbers_source: -> { ConfigFiles.editor_line_numbers },
|
|
46
46
|
diff_view: ConfigFiles.diff_view,
|
|
47
47
|
diff_view_source: -> { ConfigFiles.diff_view },
|
|
48
|
+
editor_runners_source: -> { ConfigFiles.editor_runners },
|
|
48
49
|
redraw_handler: method(:redraw_interactive_prompt)
|
|
49
50
|
)
|
|
50
51
|
if @prompt.method(:start).parameters.any? { |kind, name| [:key, :keyreq].include?(kind) && name == :render }
|
|
@@ -99,6 +100,13 @@ module Kward
|
|
|
99
100
|
@prompt.respond_to?(:start_stream_block) && @prompt.respond_to?(:write_delta)
|
|
100
101
|
end
|
|
101
102
|
|
|
103
|
+
def open_editor_for_agent(path, base_dir:, allow_new:)
|
|
104
|
+
return false unless prompt_interface?
|
|
105
|
+
return false unless @prompt.respond_to?(:edit_file)
|
|
106
|
+
|
|
107
|
+
@prompt.edit_file(path, base_dir: base_dir, allow_new: allow_new)
|
|
108
|
+
end
|
|
109
|
+
|
|
102
110
|
def emit_warning(message)
|
|
103
111
|
sink = ConfigFiles.warning_sink
|
|
104
112
|
sink ? sink.call(message) : warn(message)
|
|
@@ -129,6 +137,7 @@ module Kward
|
|
|
129
137
|
[
|
|
130
138
|
startup_status_line(refresh_update_check: refresh_update_check),
|
|
131
139
|
*startup_update_notice_lines,
|
|
140
|
+
*startup_authentication_notice_lines,
|
|
132
141
|
"",
|
|
133
142
|
startup_info_line("Workspace", startup_workspace_label),
|
|
134
143
|
startup_info_line("Branch", startup_branch_value),
|
|
@@ -198,6 +207,24 @@ module Kward
|
|
|
198
207
|
@startup_update_notice = @startup_update_check.notice(refresh: refresh)
|
|
199
208
|
end
|
|
200
209
|
|
|
210
|
+
def startup_authentication_notice_lines
|
|
211
|
+
return [] unless startup_authentication_required?
|
|
212
|
+
|
|
213
|
+
[
|
|
214
|
+
ANSI.colorize(" No model provider is connected.", :yellow, enabled: @color_enabled),
|
|
215
|
+
" Run /login to sign in, or /model to configure a local server."
|
|
216
|
+
]
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def startup_authentication_required?
|
|
220
|
+
return false unless @client.is_a?(Client)
|
|
221
|
+
return false if @client.current_provider == "Local"
|
|
222
|
+
|
|
223
|
+
auth_credentials.none? { |credential| credential.fetch(:configured) }
|
|
224
|
+
rescue StandardError
|
|
225
|
+
false
|
|
226
|
+
end
|
|
227
|
+
|
|
201
228
|
def startup_info_line(label, value)
|
|
202
229
|
"#{ANSI.colorize(label.ljust(12), :gray, enabled: @color_enabled)}#{ANSI.colorize(value, :cyan, enabled: @color_enabled)}"
|
|
203
230
|
end
|
|
@@ -220,14 +247,16 @@ module Kward
|
|
|
220
247
|
end
|
|
221
248
|
|
|
222
249
|
def prompt_footer_renderer
|
|
223
|
-
return nil unless plugin_registry.
|
|
250
|
+
return nil unless plugin_registry.status?
|
|
224
251
|
|
|
225
|
-
lambda do
|
|
226
|
-
|
|
227
|
-
next "" unless renderer
|
|
252
|
+
lambda do |max_width = nil|
|
|
253
|
+
next "" unless plugin_registry.status?
|
|
228
254
|
|
|
229
255
|
context = plugin_context(current_footer_conversation, "")
|
|
230
|
-
|
|
256
|
+
segments = plugin_registry.status_segments(context)
|
|
257
|
+
plugin_registry.compose_status(segments, max_width: max_width) do |text|
|
|
258
|
+
TerminalText.width(ANSI.strip(text.to_s))
|
|
259
|
+
end
|
|
231
260
|
rescue StandardError => e
|
|
232
261
|
emit_warning "Warning: Kward plugin footer error: #{e.message}"
|
|
233
262
|
""
|
|
@@ -244,6 +273,8 @@ module Kward
|
|
|
244
273
|
parts = []
|
|
245
274
|
git = composer_git_branch_text
|
|
246
275
|
parts << git if git
|
|
276
|
+
merge = composer_worktree_merge_text
|
|
277
|
+
parts << merge if merge
|
|
247
278
|
diff = composer_session_diff_text
|
|
248
279
|
parts << diff if diff
|
|
249
280
|
usage = composer_context_usage(provider, model)
|
|
@@ -252,6 +283,22 @@ module Kward
|
|
|
252
283
|
parts.join(" · ")
|
|
253
284
|
end
|
|
254
285
|
|
|
286
|
+
def composer_worktree_merge_text
|
|
287
|
+
return nil unless respond_to?(:active_tab, true) && respond_to?(:worktree_binding_for, true)
|
|
288
|
+
|
|
289
|
+
binding = worktree_binding_for(active_tab)
|
|
290
|
+
return nil unless binding&.active?
|
|
291
|
+
return nil unless git_worktree_manager.merge_in_progress?(binding.origin_root)
|
|
292
|
+
|
|
293
|
+
conflicts = git_worktree_manager.merge_conflict_marker_paths(binding.origin_root, cached: false)
|
|
294
|
+
target = git_worktree_manager.current_branch(binding.origin_root)
|
|
295
|
+
count = conflicts.length
|
|
296
|
+
detail = count.zero? ? "ready" : "#{count} conflict#{count == 1 ? "" : "s"}"
|
|
297
|
+
ANSI.colorize("merge→#{target.empty? ? "detached" : target} (#{detail})", :red, enabled: @color_enabled)
|
|
298
|
+
rescue GitWorktreeManager::Error
|
|
299
|
+
nil
|
|
300
|
+
end
|
|
301
|
+
|
|
255
302
|
def composer_session_diff_text
|
|
256
303
|
return nil if @session_diff.nil? || @session_diff.empty?
|
|
257
304
|
|