kward 0.84.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 +34 -0
- data/Gemfile.lock +2 -2
- data/README.md +2 -2
- data/doc/agent-tools.md +13 -1
- data/doc/api.md +17 -2
- data/doc/composer.md +1 -1
- data/doc/configuration.md +27 -4
- data/doc/extensibility.md +2 -1
- data/doc/git.md +3 -1
- data/doc/pan.md +6 -0
- data/doc/permissions.md +4 -4
- data/doc/plugins.md +464 -15
- data/doc/rpc.md +154 -16
- data/doc/sandboxing.md +11 -5
- data/doc/security.md +7 -1
- data/doc/session-management.md +5 -4
- data/doc/tabs.md +6 -2
- data/doc/transports.md +15 -0
- data/doc/usage.md +4 -1
- 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/lib/kward/agent.rb +24 -11
- data/lib/kward/cli/compaction.rb +9 -3
- data/lib/kward/cli/plugins.rb +81 -12
- data/lib/kward/cli/prompt_interface.rb +25 -5
- data/lib/kward/cli/rendering.rb +3 -0
- data/lib/kward/cli/runtime_helpers.rb +121 -27
- 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 +45 -4
- data/lib/kward/cli/tabs.rb +167 -27
- data/lib/kward/{cli_transcript_formatter.rb → cli/transcript_formatter.rb} +3 -3
- data/lib/kward/cli/worktrees.rb +65 -2
- data/lib/kward/cli.rb +26 -24
- 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 -1008
- data/lib/kward/conversation.rb +31 -2
- data/lib/kward/image_attachments.rb +1 -1
- data/lib/kward/model/client.rb +2 -2
- 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/typesafe_client.rb +78 -0
- data/lib/kward/pan/server.rb +10 -7
- 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 +1 -1
- data/lib/kward/prompt_interface/composer_state.rb +1 -1
- data/lib/kward/prompt_interface/editor/controller.rb +1 -1
- data/lib/kward/prompt_interface/editor/runner.rb +1 -1
- data/lib/kward/prompt_interface/editor/state.rb +1 -1
- data/lib/kward/prompt_interface/layout.rb +1 -1
- data/lib/kward/prompt_interface/overlay_renderer.rb +1 -1
- data/lib/kward/prompt_interface/plugin_ui_requests.rb +82 -0
- data/lib/kward/prompt_interface/runtime_state.rb +6 -1
- data/lib/kward/prompt_interface/screen.rb +9 -2
- data/lib/kward/prompt_interface.rb +60 -11
- data/lib/kward/prompts/commands.rb +2 -1
- data/lib/kward/{adaptive_pty_output_sink.rb → pty/adaptive_output_sink.rb} +1 -1
- data/lib/kward/{interactive_pty_runner.rb → pty/interactive_runner.rb} +2 -2
- 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} +9 -4
- 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/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/{kwsh.rb → shell/kwsh.rb} +3 -3
- data/lib/kward/{persistent_shell_session.rb → shell/persistent_session.rb} +3 -3
- 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/{clipboard.rb → terminal/clipboard.rb} +1 -1
- data/lib/kward/{terminal_image_support.rb → terminal/image_support.rb} +1 -1
- 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/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 +96 -13
- 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 +2 -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/{git_worktree_manager.rb → workspace/git_worktree_manager.rb} +28 -0
- data/lib/kward/{workspace.rb → workspace/workspace.rb} +3 -3
- data/templates/default/fulldoc/html/css/kward.css +125 -0
- data/templates/default/fulldoc/html/images/kward_screen_1.png +0 -0
- data/templates/default/fulldoc/html/setup.rb +1 -1
- data/templates/default/layout/html/layout.erb +16 -4
- metadata +67 -48
- data/lib/kward/tab_driver.rb +0 -90
- data/templates/default/fulldoc/html/images/kward_workflow.svg +0 -52
- /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/{markdown_code_block.rb → prompt_interface/editor/markdown_code_block.rb} +0 -0
- /data/lib/kward/{scratchpad_languages.rb → prompt_interface/editor/scratchpad_languages.rb} +0 -0
- /data/lib/kward/{scratchpad_runner.rb → prompt_interface/editor/scratchpad_runner.rb} +0 -0
- /data/lib/kward/{detached_run.rb → pty/detached_run.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/{kwshrc.rb → shell/kwshrc.rb} +0 -0
- /data/lib/kward/{shell_prompt.rb → shell/prompt.rb} +0 -0
- /data/lib/kward/{ansi.rb → terminal/ansi.rb} +0 -0
- /data/lib/kward/{terminal_keys.rb → terminal/keys.rb} +0 -0
- /data/lib/kward/{terminal_sequences.rb → terminal/sequences.rb} +0 -0
- /data/lib/kward/{terminal_text.rb → terminal/text.rb} +0 -0
- /data/lib/kward/{transcript_export.rb → transcripts/transcript_export.rb} +0 -0
- /data/lib/kward/{project_files.rb → workspace/files.rb} +0 -0
- /data/lib/kward/{path_guard.rb → workspace/path_guard.rb} +0 -0
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)
|
|
@@ -247,14 +247,16 @@ module Kward
|
|
|
247
247
|
end
|
|
248
248
|
|
|
249
249
|
def prompt_footer_renderer
|
|
250
|
-
return nil unless plugin_registry.
|
|
250
|
+
return nil unless plugin_registry.status?
|
|
251
251
|
|
|
252
|
-
lambda do
|
|
253
|
-
|
|
254
|
-
next "" unless renderer
|
|
252
|
+
lambda do |max_width = nil|
|
|
253
|
+
next "" unless plugin_registry.status?
|
|
255
254
|
|
|
256
255
|
context = plugin_context(current_footer_conversation, "")
|
|
257
|
-
|
|
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
|
|
258
260
|
rescue StandardError => e
|
|
259
261
|
emit_warning "Warning: Kward plugin footer error: #{e.message}"
|
|
260
262
|
""
|
|
@@ -271,6 +273,8 @@ module Kward
|
|
|
271
273
|
parts = []
|
|
272
274
|
git = composer_git_branch_text
|
|
273
275
|
parts << git if git
|
|
276
|
+
merge = composer_worktree_merge_text
|
|
277
|
+
parts << merge if merge
|
|
274
278
|
diff = composer_session_diff_text
|
|
275
279
|
parts << diff if diff
|
|
276
280
|
usage = composer_context_usage(provider, model)
|
|
@@ -279,6 +283,22 @@ module Kward
|
|
|
279
283
|
parts.join(" · ")
|
|
280
284
|
end
|
|
281
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
|
+
|
|
282
302
|
def composer_session_diff_text
|
|
283
303
|
return nil if @session_diff.nil? || @session_diff.empty?
|
|
284
304
|
|
data/lib/kward/cli/rendering.rb
CHANGED
|
@@ -224,6 +224,9 @@ module Kward
|
|
|
224
224
|
def print_user_transcript(input, display_input: nil, attachment_references: nil, image_parts: nil)
|
|
225
225
|
visible_input = display_input.nil? ? input : display_input
|
|
226
226
|
write_prompt_transcript("\n#{colored("❯ You>", :blue, :bold)} #{visible_input}\n")
|
|
227
|
+
# System-turn instructions are plain text, never image attachments.
|
|
228
|
+
return if input.is_a?(PluginTurnRequest)
|
|
229
|
+
|
|
227
230
|
print_attachment_badges(input, references: attachment_references)
|
|
228
231
|
print_pasted_images(input, image_parts: image_parts)
|
|
229
232
|
end
|
|
@@ -47,6 +47,11 @@ module Kward
|
|
|
47
47
|
|
|
48
48
|
def runtime_output(text)
|
|
49
49
|
content = text.to_s.chomp
|
|
50
|
+
if (tab = Thread.current[:kward_local_busy_tab]) && !content.empty?
|
|
51
|
+
tab.append_pending_runtime_output(content)
|
|
52
|
+
return
|
|
53
|
+
end
|
|
54
|
+
|
|
50
55
|
label = colored(runtime_output_prompt, :gray, :bold)
|
|
51
56
|
separator = content.include?("\n") ? "\n" : " "
|
|
52
57
|
@prompt.say("\n#{label}#{separator}#{content}\n")
|
|
@@ -61,6 +66,7 @@ module Kward
|
|
|
61
66
|
workspace: workspace,
|
|
62
67
|
prompt: @prompt,
|
|
63
68
|
skills: ConfigFiles.skills(workspace_root: conversation.workspace_root, project_skill_paths: project_skill_paths_for(conversation.workspace_root)),
|
|
69
|
+
plugin_tools: plugin_registry.tools,
|
|
64
70
|
tool_approval: interactive_tool_approval_callback,
|
|
65
71
|
hook_manager: hook_manager,
|
|
66
72
|
hook_context: hook_context
|
|
@@ -320,8 +326,13 @@ module Kward
|
|
|
320
326
|
@prompt.say(intro_message)
|
|
321
327
|
end
|
|
322
328
|
origin_tab = active_tab if respond_to?(:active_tab, true)
|
|
323
|
-
result = run_interactive_pty_with_terminal_handoff(shell, command, env: env, cwd: cwd) do |sink, completed_result|
|
|
324
|
-
transcript_output = record_completed_pty_output(
|
|
329
|
+
result = run_interactive_pty_with_terminal_handoff(shell, command, env: env, cwd: cwd) do |sink, completed_result, background_state|
|
|
330
|
+
transcript_output = record_completed_pty_output(
|
|
331
|
+
sink,
|
|
332
|
+
completed_result,
|
|
333
|
+
tab: origin_tab,
|
|
334
|
+
background_state: background_state
|
|
335
|
+
)
|
|
325
336
|
yield(transcript_output, completed_result) if block_given?
|
|
326
337
|
end
|
|
327
338
|
refresh_composer_status
|
|
@@ -337,7 +348,13 @@ module Kward
|
|
|
337
348
|
with_interactive_terminal_handoff do |input, sink|
|
|
338
349
|
tab_action_handler = terminal_handoff_tab_action_handler
|
|
339
350
|
on_detach = if origin_tab
|
|
340
|
-
|
|
351
|
+
lambda do
|
|
352
|
+
BufferedPtyOutputSink.new(
|
|
353
|
+
max_capture_bytes: Kward::CLI::Tabs::TRANSIENT_SHELL_OUTPUT_LIMIT,
|
|
354
|
+
initial_output: sink.captured_output,
|
|
355
|
+
initial_truncated: sink.truncated?
|
|
356
|
+
)
|
|
357
|
+
end
|
|
341
358
|
end
|
|
342
359
|
result = if shell.respond_to?(:run_interactive)
|
|
343
360
|
shell.run_interactive(command, input: input, sink: sink, tab_action_handler: tab_action_handler, on_detach: on_detach)
|
|
@@ -345,7 +362,7 @@ module Kward
|
|
|
345
362
|
runner.run(shell, "-c", command, env: env, cwd: cwd, input: input, sink: sink, tab_action_handler: tab_action_handler, on_detach: on_detach)
|
|
346
363
|
end
|
|
347
364
|
if result&.background
|
|
348
|
-
register_background_tab_run(origin_tab, result.background, &on_complete)
|
|
365
|
+
register_background_tab_run(origin_tab, result.background, output_kind: :pty, &on_complete)
|
|
349
366
|
else
|
|
350
367
|
on_complete.call(sink, result) if on_complete
|
|
351
368
|
end
|
|
@@ -404,7 +421,76 @@ module Kward
|
|
|
404
421
|
end
|
|
405
422
|
end
|
|
406
423
|
|
|
407
|
-
def record_completed_pty_output(sink, result, tab: nil)
|
|
424
|
+
def record_completed_pty_output(sink, result, tab: nil, background_state: nil)
|
|
425
|
+
transcript_output = background_shell_transcript_output(sink, result)
|
|
426
|
+
return unless transcript_output
|
|
427
|
+
|
|
428
|
+
rendered_output = background_state&.fetch(:rendered_output, "").to_s
|
|
429
|
+
delta = if transcript_output.start_with?(rendered_output)
|
|
430
|
+
transcript_output[rendered_output.length..].to_s
|
|
431
|
+
else
|
|
432
|
+
transcript_output
|
|
433
|
+
end
|
|
434
|
+
if background_state
|
|
435
|
+
background_state[:rendered_output] = transcript_output
|
|
436
|
+
render_background_tab_delta(tab, delta) unless delta.empty?
|
|
437
|
+
else
|
|
438
|
+
record_tab_transient_shell_output(transcript_output, render: false, tab: tab)
|
|
439
|
+
if @prompt.respond_to?(:record_transient_terminal_output) && (!tab || tab == active_tab)
|
|
440
|
+
@prompt.record_transient_terminal_output(transcript_output, render: false)
|
|
441
|
+
end
|
|
442
|
+
end
|
|
443
|
+
transcript_output
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
def flush_background_tab_output(tab, state, result: nil)
|
|
447
|
+
sink = state[:run].sink
|
|
448
|
+
output = sink&.captured_output.to_s
|
|
449
|
+
return if output.empty?
|
|
450
|
+
|
|
451
|
+
transcript_output = if state[:output_kind] == :streamed
|
|
452
|
+
background_stream_transcript_output(sink)
|
|
453
|
+
else
|
|
454
|
+
transcript_result = result || InteractivePtyRunner::Result.new(exit_status: 0, input_forwarded: false)
|
|
455
|
+
background_shell_transcript_output(sink, transcript_result)
|
|
456
|
+
end
|
|
457
|
+
return unless transcript_output
|
|
458
|
+
|
|
459
|
+
rendered_output = state[:rendered_output].to_s
|
|
460
|
+
if transcript_output.start_with?(rendered_output)
|
|
461
|
+
delta = transcript_output[rendered_output.length..].to_s
|
|
462
|
+
return if delta.empty?
|
|
463
|
+
|
|
464
|
+
state[:rendered_output] = transcript_output
|
|
465
|
+
render_background_tab_delta(tab, delta)
|
|
466
|
+
else
|
|
467
|
+
state[:rendered_output] = transcript_output
|
|
468
|
+
render_tab(tab, restore_composer: false) if tab == active_tab
|
|
469
|
+
end
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
def render_background_tab_delta(tab, delta)
|
|
473
|
+
return unless tab == active_tab
|
|
474
|
+
|
|
475
|
+
if @prompt.respond_to?(:record_transient_terminal_output)
|
|
476
|
+
@prompt.record_transient_terminal_output(delta)
|
|
477
|
+
elsif @prompt.respond_to?(:say)
|
|
478
|
+
@prompt.say(delta)
|
|
479
|
+
end
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
def background_stream_transcript_output(sink)
|
|
483
|
+
return if sink&.truncated?
|
|
484
|
+
|
|
485
|
+
text = ANSI.normalize_transcript_encoding(sink&.captured_output.to_s).gsub("\r\n", "\n")
|
|
486
|
+
sanitized = PtyTranscriptNormalizer.normalize(text)
|
|
487
|
+
return if sanitized.empty?
|
|
488
|
+
return if ANSI.strip_control_sequences(sanitized).match?(UNSAFE_TRANSCRIPT_CONTROL_PATTERN)
|
|
489
|
+
|
|
490
|
+
sanitized
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
def background_shell_transcript_output(sink, result)
|
|
408
494
|
classified_output = sink.respond_to?(:transcript_safe?)
|
|
409
495
|
return if classified_output && !sink.transcript_safe?
|
|
410
496
|
|
|
@@ -412,20 +498,13 @@ module Kward
|
|
|
412
498
|
allow_input = classified_output &&
|
|
413
499
|
sink.respond_to?(:pre_input_capture_only?) &&
|
|
414
500
|
sink.pre_input_capture_only?
|
|
415
|
-
|
|
501
|
+
terminal_transcript_output(
|
|
416
502
|
output,
|
|
417
503
|
result,
|
|
418
504
|
truncated: sink&.truncated? || false,
|
|
419
505
|
allow_input: allow_input,
|
|
420
506
|
normalize_line_controls: classified_output
|
|
421
507
|
)
|
|
422
|
-
return unless transcript_output
|
|
423
|
-
|
|
424
|
-
record_tab_transient_shell_output(transcript_output, render: false, tab: tab)
|
|
425
|
-
if @prompt.respond_to?(:record_transient_terminal_output) && (!tab || tab == active_tab)
|
|
426
|
-
@prompt.record_transient_terminal_output(transcript_output, render: false)
|
|
427
|
-
end
|
|
428
|
-
transcript_output
|
|
429
508
|
end
|
|
430
509
|
|
|
431
510
|
def terminal_transcript_output(
|
|
@@ -602,6 +681,7 @@ module Kward
|
|
|
602
681
|
chunks = Queue.new
|
|
603
682
|
queued_inputs = []
|
|
604
683
|
origin_tab = active_tab if respond_to?(:active_tab, true)
|
|
684
|
+
streamed_output = BufferedPtyOutputSink.new(max_capture_bytes: Kward::CLI::Tabs::TRANSIENT_SHELL_OUTPUT_LIMIT)
|
|
605
685
|
detached_sink = nil
|
|
606
686
|
result = nil
|
|
607
687
|
error = nil
|
|
@@ -610,6 +690,7 @@ module Kward
|
|
|
610
690
|
if detached_sink
|
|
611
691
|
detached_sink.write(chunk)
|
|
612
692
|
else
|
|
693
|
+
streamed_output.write(chunk)
|
|
613
694
|
chunks << chunk
|
|
614
695
|
end
|
|
615
696
|
end
|
|
@@ -626,17 +707,18 @@ module Kward
|
|
|
626
707
|
elsif poll_result.is_a?(Hash) && poll_result[:tab_action]
|
|
627
708
|
(@pending_inputs ||= []).unshift(poll_result)
|
|
628
709
|
if origin_tab
|
|
629
|
-
detached_sink = BufferedPtyOutputSink.new(
|
|
710
|
+
detached_sink = BufferedPtyOutputSink.new(
|
|
711
|
+
max_capture_bytes: Kward::CLI::Tabs::TRANSIENT_SHELL_OUTPUT_LIMIT,
|
|
712
|
+
initial_output: streamed_output.captured_output,
|
|
713
|
+
initial_truncated: streamed_output.truncated?
|
|
714
|
+
)
|
|
630
715
|
background = DetachedRun.new(sink: detached_sink, canceler: -> { cancellation.cancel! }) do
|
|
631
716
|
worker.join
|
|
632
717
|
raise error if error
|
|
633
718
|
|
|
634
719
|
result
|
|
635
720
|
end
|
|
636
|
-
register_background_tab_run(origin_tab, background
|
|
637
|
-
record_tab_transient_shell_output(completed_result.output, render: false, tab: origin_tab)
|
|
638
|
-
@prompt.say(completed_result.output) if origin_tab == active_tab && completed_result.output.to_s != ""
|
|
639
|
-
end
|
|
721
|
+
register_background_tab_run(origin_tab, background, output_kind: :streamed)
|
|
640
722
|
return Kwsh::Result.new(output: "", exit_status: 0, streamed: true)
|
|
641
723
|
end
|
|
642
724
|
cancellation.cancel!
|
|
@@ -737,44 +819,56 @@ module Kward
|
|
|
737
819
|
queued_inputs = []
|
|
738
820
|
result = nil
|
|
739
821
|
error = nil
|
|
740
|
-
|
|
822
|
+
cancelled = false
|
|
823
|
+
origin_tab = active_tab
|
|
824
|
+
cancellation = Cancellation.new
|
|
825
|
+
begin_local_busy_command(activity, tab: origin_tab)
|
|
741
826
|
|
|
742
827
|
worker = Thread.new do
|
|
743
|
-
|
|
828
|
+
Thread.current[:kward_local_busy_tab] = origin_tab
|
|
829
|
+
result = yield(cancellation)
|
|
744
830
|
rescue StandardError => e
|
|
745
831
|
error = e
|
|
832
|
+
ensure
|
|
833
|
+
Thread.current[:kward_local_busy_tab] = nil
|
|
746
834
|
end
|
|
747
835
|
|
|
748
836
|
while worker.alive?
|
|
749
837
|
poll_result = collect_queued_input(queued_inputs)
|
|
838
|
+
if poll_result == PromptInterface::CANCEL_INPUT && !cancelled
|
|
839
|
+
cancelled = true
|
|
840
|
+
cancellation.cancel!
|
|
841
|
+
end
|
|
750
842
|
handle_busy_local_tab_action(poll_result, activity: activity) if busy_local_tab_action?(poll_result)
|
|
751
843
|
sleep 0.02
|
|
752
844
|
end
|
|
753
845
|
worker.join
|
|
754
846
|
drain_queued_input(queued_inputs)
|
|
755
|
-
|
|
847
|
+
flushed = flush_pending_runtime_outputs(origin_tab)
|
|
848
|
+
origin_tab.unread = true if origin_tab && !flushed && origin_tab.pending_runtime_outputs.any?
|
|
849
|
+
raise error if error && !error.is_a?(Cancellation::CancelledError)
|
|
756
850
|
|
|
757
851
|
[result, queued_inputs]
|
|
758
852
|
ensure
|
|
759
|
-
finish_local_busy_command(activity)
|
|
853
|
+
finish_local_busy_command(activity, tab: origin_tab)
|
|
760
854
|
end
|
|
761
855
|
|
|
762
856
|
def run_busy_local_command_and_requeue(activity: "loading")
|
|
763
857
|
return yield unless prompt_interface?
|
|
764
858
|
|
|
765
|
-
result, queued_inputs = run_busy_local_command(activity: activity) { yield }
|
|
859
|
+
result, queued_inputs = run_busy_local_command(activity: activity) { |cancellation| yield(cancellation) }
|
|
766
860
|
queued_inputs.reverse_each { |pending_input| @pending_inputs.unshift(pending_input) }
|
|
767
861
|
result
|
|
768
862
|
end
|
|
769
863
|
|
|
770
|
-
def begin_local_busy_command(activity)
|
|
771
|
-
|
|
864
|
+
def begin_local_busy_command(activity, tab: active_tab)
|
|
865
|
+
tab.local_busy_activity = activity if tab
|
|
772
866
|
@prompt.begin_busy_input("You>", activity: activity) if @prompt.respond_to?(:begin_busy_input)
|
|
773
867
|
update_prompt_tabs if respond_to?(:update_prompt_tabs, true)
|
|
774
868
|
end
|
|
775
869
|
|
|
776
|
-
def finish_local_busy_command(activity)
|
|
777
|
-
tab
|
|
870
|
+
def finish_local_busy_command(activity, tab: nil)
|
|
871
|
+
tab ||= local_busy_tab(activity)
|
|
778
872
|
tab.local_busy_activity = nil if tab
|
|
779
873
|
update_prompt_tabs if respond_to?(:update_prompt_tabs, true)
|
|
780
874
|
@prompt.finish_busy_input if prompt_interface? && @prompt.respond_to?(:finish_busy_input) && (!active_tab || active_tab == tab)
|
data/lib/kward/cli/sessions.rb
CHANGED
|
@@ -392,13 +392,15 @@ module Kward
|
|
|
392
392
|
end
|
|
393
393
|
|
|
394
394
|
def reload_active_session(session_store)
|
|
395
|
+
workspace_root = current_workspace_root
|
|
396
|
+
worktree = worktree_binding_for(active_tab)
|
|
395
397
|
@active_session, conversation = session_store.load(
|
|
396
398
|
@active_session.path,
|
|
397
|
-
workspace: configured_workspace(root:
|
|
399
|
+
workspace: configured_workspace(root: workspace_root, strict: worktree&.active? == true),
|
|
398
400
|
provider: current_model_provider,
|
|
399
401
|
model: current_model_id,
|
|
400
402
|
reasoning_effort: current_reasoning_effort,
|
|
401
|
-
project_skill_paths: project_skill_paths_for(
|
|
403
|
+
project_skill_paths: project_skill_paths_for(workspace_root) || []
|
|
402
404
|
)
|
|
403
405
|
reset_session_diff(@active_session.path)
|
|
404
406
|
track_session(@active_session)
|
|
@@ -415,7 +417,7 @@ module Kward
|
|
|
415
417
|
SessionTreeRenderer.new(roots: roots, current_leaf_id: current_leaf_id).items
|
|
416
418
|
end
|
|
417
419
|
|
|
418
|
-
def rename_session(argument, require_name: false)
|
|
420
|
+
def rename_session(argument, require_name: false, announce: true)
|
|
419
421
|
unless @active_session
|
|
420
422
|
runtime_output("No active persisted session.")
|
|
421
423
|
return
|
|
@@ -430,8 +432,10 @@ module Kward
|
|
|
430
432
|
old_name = @active_session.name
|
|
431
433
|
@active_session.rename(name)
|
|
432
434
|
run_lifecycle_hook("session_rename", conversation: new_conversation(workspace_root: current_workspace_root), session: @active_session, payload: { old_name: old_name, new_name: @active_session.name })
|
|
433
|
-
|
|
434
|
-
|
|
435
|
+
if announce
|
|
436
|
+
label = @active_session.name ? "Named session: #{@active_session.name}" : "Cleared session name."
|
|
437
|
+
runtime_output(label)
|
|
438
|
+
end
|
|
435
439
|
end
|
|
436
440
|
|
|
437
441
|
def clone_session(session_store, agent)
|