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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
require_relative "../scratchpad_languages"
|
|
1
|
+
require_relative "../prompt_interface/editor/scratchpad_languages"
|
|
2
2
|
|
|
3
3
|
# Namespace for the Kward CLI agent runtime.
|
|
4
4
|
module Kward
|
|
@@ -64,6 +64,9 @@ module Kward
|
|
|
64
64
|
[true, nil]
|
|
65
65
|
when "tab"
|
|
66
66
|
[true, handle_tab_command(argument, session_store)]
|
|
67
|
+
when "name"
|
|
68
|
+
rename_active_session_and_tab(argument)
|
|
69
|
+
[true, nil]
|
|
67
70
|
when "worktree"
|
|
68
71
|
handle_worktree_command(argument)
|
|
69
72
|
[true, active_tab&.agent]
|
|
@@ -81,7 +84,9 @@ module Kward
|
|
|
81
84
|
configure_reasoning(agent.conversation)
|
|
82
85
|
[true, nil]
|
|
83
86
|
when "reload"
|
|
84
|
-
|
|
87
|
+
strict_worktree = active_tab&.driver&.respond_to?(:worktree) && active_tab.driver.worktree&.active?
|
|
88
|
+
registry = strict_worktree ? nil : agent.tool_registry
|
|
89
|
+
run_busy_local_command_and_requeue { reload_plugins(agent.conversation, tool_registry: registry) }
|
|
85
90
|
[true, nil]
|
|
86
91
|
when "new"
|
|
87
92
|
[true, run_busy_local_command_and_requeue { start_new_session(session_store) }]
|
|
@@ -113,7 +118,11 @@ module Kward
|
|
|
113
118
|
run_busy_local_command_and_requeue { export_session(agent.conversation, argument) }
|
|
114
119
|
[true, nil]
|
|
115
120
|
when "compact"
|
|
116
|
-
|
|
121
|
+
origin_tab = active_tab
|
|
122
|
+
result = run_busy_local_command_and_requeue(activity: "compacting") do |cancellation|
|
|
123
|
+
compact_context(agent, argument, cancellation: cancellation)
|
|
124
|
+
end
|
|
125
|
+
render_compaction_summary(result) if !origin_tab || origin_tab == active_tab
|
|
117
126
|
[true, nil]
|
|
118
127
|
else
|
|
119
128
|
if name.to_s.start_with?("skill:")
|
|
@@ -122,7 +131,7 @@ module Kward
|
|
|
122
131
|
elsif interactive_command_for(name) && prompt_interface? && @prompt.respond_to?(:start_interactive)
|
|
123
132
|
run_interactive_command(name, argument, agent)
|
|
124
133
|
elsif plugin_command_for(name)
|
|
125
|
-
|
|
134
|
+
run_plugin_command_and_turn(name, argument, agent)
|
|
126
135
|
else
|
|
127
136
|
[false, nil]
|
|
128
137
|
end
|
|
@@ -133,6 +142,38 @@ module Kward
|
|
|
133
142
|
PromptCommands.parse(command) || [nil, ""]
|
|
134
143
|
end
|
|
135
144
|
|
|
145
|
+
def rename_active_session_and_tab(argument)
|
|
146
|
+
name = argument.to_s.strip
|
|
147
|
+
return runtime_output("Usage: /name <name>") if name.empty?
|
|
148
|
+
|
|
149
|
+
tab = active_tab
|
|
150
|
+
unless @active_session && (!tab || tab.session.equal?(@active_session))
|
|
151
|
+
return runtime_output("The /name command is only available in a session tab.")
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
errors = []
|
|
155
|
+
begin
|
|
156
|
+
rename_session(name, require_name: true, announce: false)
|
|
157
|
+
rescue StandardError => error
|
|
158
|
+
errors << "session: #{error.message}"
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
if tab
|
|
162
|
+
begin
|
|
163
|
+
rename_active_tab(name)
|
|
164
|
+
rescue StandardError => error
|
|
165
|
+
errors << "tab: #{error.message}"
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
if errors.empty?
|
|
170
|
+
label = tab ? "Named session and tab: #{name}" : "Named session: #{name}"
|
|
171
|
+
runtime_output(label)
|
|
172
|
+
else
|
|
173
|
+
runtime_output("Name update incomplete (#{errors.join("; ")}).")
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
136
177
|
def handle_sandbox_command(argument, workspace_root)
|
|
137
178
|
action, value = argument.to_s.strip.split(/\s+/, 2)
|
|
138
179
|
case action
|
data/lib/kward/cli/tabs.rb
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require "json"
|
|
2
|
+
require "securerandom"
|
|
2
3
|
require "thread"
|
|
3
4
|
require_relative "../cancellation"
|
|
4
5
|
|
|
@@ -10,14 +11,16 @@ module Kward
|
|
|
10
11
|
module Tabs
|
|
11
12
|
WORKTREE_AGENT_CONTEXT = <<~PROMPT.strip.freeze
|
|
12
13
|
This is an active Git worktree tab. The workspace is sandboxed separately from the host repository metadata.
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
Workspace tools default to the active worktree. When they advertise `target`, use `target: "origin"` to inspect the verified host repository worktree without leaving this tab. Read the origin's applicable `AGENTS.md` before changing it.
|
|
15
|
+
When the user explicitly asks you to commit, use the advertised `git_commit` tool. Set `target: "origin"` only when the user asks to commit there. Do not use `run_shell_command` for `git add` or `git commit`, because shared Git metadata is intentionally protected.
|
|
16
|
+
`git_commit` requires a commit message and accepts optional target-relative paths; omit paths to include all current changes in the selected worktree. Do not create a commit unless the user requested one.
|
|
15
17
|
PROMPT
|
|
16
18
|
|
|
17
19
|
TabRuntime = Struct.new(
|
|
18
20
|
:session,
|
|
19
21
|
:agent,
|
|
20
22
|
:driver,
|
|
23
|
+
:plugin_host,
|
|
21
24
|
:diff,
|
|
22
25
|
:snapshot,
|
|
23
26
|
:status,
|
|
@@ -41,6 +44,7 @@ module Kward
|
|
|
41
44
|
:transient_shell_entries,
|
|
42
45
|
:error_reported,
|
|
43
46
|
:local_busy_activity,
|
|
47
|
+
:pending_runtime_outputs,
|
|
44
48
|
keyword_init: true
|
|
45
49
|
) do
|
|
46
50
|
TRANSIENT_SHELL_OUTPUT_LIMIT = 200_000
|
|
@@ -53,6 +57,11 @@ module Kward
|
|
|
53
57
|
!local_busy_activity.to_s.empty?
|
|
54
58
|
end
|
|
55
59
|
|
|
60
|
+
def append_pending_runtime_output(text)
|
|
61
|
+
self.pending_runtime_outputs ||= []
|
|
62
|
+
pending_runtime_outputs << text.to_s
|
|
63
|
+
end
|
|
64
|
+
|
|
56
65
|
def idle?
|
|
57
66
|
!running? && !local_busy?
|
|
58
67
|
end
|
|
@@ -205,14 +214,23 @@ module Kward
|
|
|
205
214
|
end
|
|
206
215
|
|
|
207
216
|
tab_type = plugin_registry.tab_type_for_id(descriptor["plugin_tab_type"])
|
|
217
|
+
host = nil
|
|
208
218
|
driver = if tab_type&.local
|
|
209
|
-
|
|
210
|
-
tab_type.
|
|
219
|
+
descriptor["scope_key"] ||= plugin_tab_scope_key(tab_type)
|
|
220
|
+
host = build_plugin_tab_host(tab_type, workspace_root: session_store.cwd, scope_key: descriptor["scope_key"])
|
|
221
|
+
begin
|
|
222
|
+
created_driver = tab_type.handler.call(host, descriptor)
|
|
223
|
+
raise "Plugin tab #{descriptor["plugin_tab_type"].inspect} did not return a tab driver." unless created_driver
|
|
224
|
+
|
|
225
|
+
tab_type.capabilities.validate_driver!(created_driver)
|
|
226
|
+
rescue StandardError
|
|
227
|
+
host.shutdown
|
|
228
|
+
raise
|
|
229
|
+
end
|
|
211
230
|
else
|
|
212
231
|
UnavailableTabDriver.new(descriptor: descriptor, message: "Plugin tab #{descriptor["plugin_tab_type"].inspect} is unavailable.")
|
|
213
232
|
end
|
|
214
|
-
|
|
215
|
-
build_tab(nil, nil, driver: driver, label: label || descriptor["label"] || tab_type&.title)
|
|
233
|
+
build_tab(nil, nil, driver: driver, plugin_host: host, label: label || descriptor["label"] || tab_type&.title)
|
|
216
234
|
end
|
|
217
235
|
|
|
218
236
|
def open_plugin_tab(name, session_store)
|
|
@@ -225,12 +243,20 @@ module Kward
|
|
|
225
243
|
|
|
226
244
|
save_active_tab_state
|
|
227
245
|
stop_tab_live_view
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
246
|
+
scope_key = plugin_tab_scope_key(tab_type)
|
|
247
|
+
descriptor = { "kind" => "plugin", "plugin_tab_type" => tab_type.id, "label" => tab_type.title, "scope_key" => scope_key }
|
|
248
|
+
host = build_plugin_tab_host(tab_type, workspace_root: session_store.cwd, scope_key: scope_key)
|
|
249
|
+
begin
|
|
250
|
+
driver = tab_type.handler.call(host, descriptor)
|
|
251
|
+
raise "Plugin tab #{name.inspect} did not return a tab driver." unless driver
|
|
252
|
+
|
|
253
|
+
tab_type.capabilities.validate_driver!(driver)
|
|
254
|
+
rescue StandardError
|
|
255
|
+
host.shutdown
|
|
256
|
+
raise
|
|
257
|
+
end
|
|
232
258
|
|
|
233
|
-
@tabs << build_tab(nil, nil, driver: driver, label: tab_type.title)
|
|
259
|
+
@tabs << build_tab(nil, nil, driver: driver, plugin_host: host, label: tab_type.title)
|
|
234
260
|
@active_tab_index = @tabs.length - 1
|
|
235
261
|
activate_tab(@active_tab_index)
|
|
236
262
|
rescue StandardError => e
|
|
@@ -254,21 +280,30 @@ module Kward
|
|
|
254
280
|
strict = worktree&.active? == true
|
|
255
281
|
update_worktree_agent_context(conversation, strict)
|
|
256
282
|
workspace = configured_workspace(root: conversation.workspace_root, strict: strict)
|
|
283
|
+
workspace_targets = if strict
|
|
284
|
+
origin_workspace = configured_workspace(root: worktree.origin_root, strict: true)
|
|
285
|
+
Tools::WorkspaceTargets.new(active: workspace, targets: { origin: origin_workspace })
|
|
286
|
+
end
|
|
257
287
|
prompt = TabQuestionPrompt.new(self)
|
|
258
288
|
hook_manager = strict ? nil : lifecycle_hook_manager(conversation)
|
|
259
289
|
hook_context = strict ? nil : lifecycle_hook_context(conversation)
|
|
260
290
|
git_committer = if strict
|
|
261
|
-
|
|
291
|
+
lambda do |message:, paths:, target: Tools::WorkspaceTargets::ACTIVE|
|
|
292
|
+
commit_workspace = target == "origin" ? worktree.origin_root : workspace.root.to_s
|
|
293
|
+
git_commit_for_agent(commit_workspace, message: message, paths: paths)
|
|
294
|
+
end
|
|
262
295
|
end
|
|
263
296
|
project_skill_paths = project_skill_paths_for(conversation.workspace_root) || []
|
|
264
297
|
tool_registry = ToolRegistry.new(
|
|
265
298
|
workspace: workspace,
|
|
299
|
+
workspace_targets: workspace_targets,
|
|
266
300
|
prompt: prompt,
|
|
267
301
|
skills: ConfigFiles.skills(workspace_root: conversation.workspace_root, project_skill_paths: project_skill_paths),
|
|
268
302
|
tool_approval: tab_tool_approval_callback(prompt),
|
|
269
303
|
hook_manager: hook_manager,
|
|
270
304
|
hook_context: hook_context,
|
|
271
305
|
mcp_clients: strict ? [] : nil,
|
|
306
|
+
plugin_tools: strict ? [] : plugin_registry.tools,
|
|
272
307
|
git_committer: git_committer
|
|
273
308
|
)
|
|
274
309
|
@footer_conversation = conversation
|
|
@@ -301,12 +336,13 @@ module Kward
|
|
|
301
336
|
end
|
|
302
337
|
end
|
|
303
338
|
|
|
304
|
-
def build_tab(session, agent, driver: nil, label: nil)
|
|
339
|
+
def build_tab(session, agent, driver: nil, plugin_host: nil, label: nil)
|
|
305
340
|
driver ||= SessionTabDriver.new(session: session, agent: agent)
|
|
306
341
|
TabRuntime.new(
|
|
307
342
|
session: session,
|
|
308
343
|
agent: agent,
|
|
309
344
|
driver: driver,
|
|
345
|
+
plugin_host: plugin_host,
|
|
310
346
|
diff: session&.path ? SessionDiff.from_session_file(session.path) : SessionDiff.new,
|
|
311
347
|
snapshot: nil,
|
|
312
348
|
status: "idle",
|
|
@@ -329,7 +365,8 @@ module Kward
|
|
|
329
365
|
background_run: nil,
|
|
330
366
|
transient_shell_entries: [],
|
|
331
367
|
error_reported: false,
|
|
332
|
-
local_busy_activity: nil
|
|
368
|
+
local_busy_activity: nil,
|
|
369
|
+
pending_runtime_outputs: []
|
|
333
370
|
).tap { |tab| assign_tab_question_prompt(agent, tab) }
|
|
334
371
|
end
|
|
335
372
|
|
|
@@ -429,6 +466,7 @@ module Kward
|
|
|
429
466
|
|
|
430
467
|
if @tabs.length <= 1
|
|
431
468
|
close_kwsh_session(tab.shell) if tab&.shell && respond_to?(:close_kwsh_session, true)
|
|
469
|
+
close_plugin_tab(tab)
|
|
432
470
|
@tabs.clear
|
|
433
471
|
persist_tabs
|
|
434
472
|
return PromptInterface::EXIT_INPUT
|
|
@@ -437,7 +475,7 @@ module Kward
|
|
|
437
475
|
stop_tab_live_view
|
|
438
476
|
close_kwsh_session(tab.shell) if tab&.shell && respond_to?(:close_kwsh_session, true)
|
|
439
477
|
tab.session&.delete_if_unused if tab&.session.respond_to?(:delete_if_unused)
|
|
440
|
-
tab
|
|
478
|
+
close_plugin_tab(tab)
|
|
441
479
|
@tabs.delete_at(@active_tab_index)
|
|
442
480
|
@active_tab_index = [@active_tab_index, @tabs.length - 1].min
|
|
443
481
|
activate_tab(@active_tab_index)
|
|
@@ -555,8 +593,23 @@ module Kward
|
|
|
555
593
|
end
|
|
556
594
|
report_tab_runtime_error(tab) if %w[failed cancelled].include?(tab.status.to_s)
|
|
557
595
|
render_tab_transient_shell_entries(tab)
|
|
596
|
+
render_background_tab_output(tab)
|
|
558
597
|
end
|
|
559
598
|
restore_tab_composer_snapshot(tab.snapshot) if restore_composer
|
|
599
|
+
render_pending_tab_runtime_outputs(tab)
|
|
600
|
+
end
|
|
601
|
+
|
|
602
|
+
def render_pending_tab_runtime_outputs(tab)
|
|
603
|
+
outputs = Array(tab.pending_runtime_outputs)
|
|
604
|
+
tab.pending_runtime_outputs = []
|
|
605
|
+
outputs.each { |output| runtime_output(output) }
|
|
606
|
+
end
|
|
607
|
+
|
|
608
|
+
def flush_pending_runtime_outputs(tab)
|
|
609
|
+
return false unless tab && tab == active_tab
|
|
610
|
+
|
|
611
|
+
render_pending_tab_runtime_outputs(tab)
|
|
612
|
+
true
|
|
560
613
|
end
|
|
561
614
|
|
|
562
615
|
def render_tab_transient_shell_entries(tab)
|
|
@@ -569,6 +622,17 @@ module Kward
|
|
|
569
622
|
end
|
|
570
623
|
end
|
|
571
624
|
|
|
625
|
+
def render_background_tab_output(tab)
|
|
626
|
+
output = tab.background_run&.fetch(:rendered_output, "").to_s
|
|
627
|
+
return if output.empty?
|
|
628
|
+
|
|
629
|
+
if @prompt.respond_to?(:say)
|
|
630
|
+
@prompt.say(output)
|
|
631
|
+
elsif @prompt.respond_to?(:write_transcript)
|
|
632
|
+
@prompt.write_transcript(output)
|
|
633
|
+
end
|
|
634
|
+
end
|
|
635
|
+
|
|
572
636
|
def clear_active_tab_transient_shell_entries
|
|
573
637
|
active_tab&.clear_transient_shell_entries
|
|
574
638
|
end
|
|
@@ -614,10 +678,15 @@ module Kward
|
|
|
614
678
|
index ? index + 1 : active_tab_number
|
|
615
679
|
end
|
|
616
680
|
|
|
617
|
-
def register_background_tab_run(tab, run, &completion)
|
|
681
|
+
def register_background_tab_run(tab, run, output_kind: nil, &completion)
|
|
618
682
|
return unless tab && run
|
|
619
683
|
|
|
620
|
-
tab.background_run = {
|
|
684
|
+
tab.background_run = {
|
|
685
|
+
run: run,
|
|
686
|
+
completion: completion,
|
|
687
|
+
output_kind: output_kind,
|
|
688
|
+
rendered_output: +""
|
|
689
|
+
}
|
|
621
690
|
tab.local_busy_activity = "running"
|
|
622
691
|
tab.attention = nil
|
|
623
692
|
update_prompt_tabs
|
|
@@ -628,11 +697,19 @@ module Kward
|
|
|
628
697
|
|
|
629
698
|
@tabs.each do |tab|
|
|
630
699
|
state = tab.background_run
|
|
631
|
-
next unless state
|
|
700
|
+
next unless state
|
|
632
701
|
|
|
633
|
-
|
|
702
|
+
complete = state[:run].complete?
|
|
703
|
+
result = state[:run].result if complete
|
|
704
|
+
if state[:output_kind] && respond_to?(:flush_background_tab_output, true)
|
|
705
|
+
flush_background_tab_output(tab, state, result: result)
|
|
706
|
+
end
|
|
707
|
+
next unless complete
|
|
708
|
+
|
|
709
|
+
state[:completion]&.call(state[:run].sink, result, state) if result.is_a?(Struct)
|
|
710
|
+
rendered_output = state[:rendered_output].to_s
|
|
711
|
+
tab.append_transient_shell_entry(rendered_output) unless rendered_output.empty?
|
|
634
712
|
tab.background_run = nil
|
|
635
|
-
state[:completion]&.call(state[:run].sink, result) if result.is_a?(Struct)
|
|
636
713
|
finish_local_busy_command_for_tab(tab)
|
|
637
714
|
if tab != active_tab
|
|
638
715
|
tab.unread = true
|
|
@@ -654,20 +731,39 @@ module Kward
|
|
|
654
731
|
|
|
655
732
|
def submit_tab_input(tab, input, display_input: nil)
|
|
656
733
|
return if input.to_s.strip.empty?
|
|
734
|
+
return unless plugin_tab_attachments_allowed?(tab, input)
|
|
657
735
|
|
|
658
736
|
save_active_tab_state
|
|
659
737
|
start_tab_turn(tab, input, display_input: display_input)
|
|
660
738
|
start_tab_live_view(tab) if tab == active_tab
|
|
661
739
|
end
|
|
662
740
|
|
|
741
|
+
def plugin_tab_attachments_allowed?(tab, input)
|
|
742
|
+
capabilities = tab.plugin_host&.capabilities
|
|
743
|
+
return true unless capabilities&.declared?
|
|
744
|
+
return true if capabilities.allows_attachment?(:image)
|
|
745
|
+
|
|
746
|
+
attached = ImageAttachments.references_from_text(input.to_s).any? { |reference| reference[:status] == :attached }
|
|
747
|
+
return true unless attached
|
|
748
|
+
|
|
749
|
+
runtime_output("This plugin chat does not allow image attachments.")
|
|
750
|
+
false
|
|
751
|
+
end
|
|
752
|
+
|
|
663
753
|
def start_tab_turn(tab, input, display_input: nil)
|
|
664
754
|
prepare_memory_context(tab.agent.conversation, input) if tab.agent.respond_to?(:conversation)
|
|
665
|
-
print_user_transcript(input, display_input: display_input) if prompt_interface?
|
|
755
|
+
print_user_transcript(input, display_input: display_input) if prompt_interface? && tab == active_tab
|
|
666
756
|
tab.status = "queued"
|
|
667
757
|
tab.unread = false
|
|
668
758
|
tab.attention = nil
|
|
669
759
|
tab.cancellation = Cancellation.new
|
|
670
|
-
|
|
760
|
+
steering_capability = tab.plugin_host&.capabilities
|
|
761
|
+
driver_supports_steering = if steering_capability&.declared?
|
|
762
|
+
steering_capability.steering?
|
|
763
|
+
else
|
|
764
|
+
tab.driver.supports_steering?
|
|
765
|
+
end
|
|
766
|
+
tab.steering = driver_supports_steering && steering_supported? ? Steering.new : nil
|
|
671
767
|
tab.error = nil
|
|
672
768
|
tab.answer = nil
|
|
673
769
|
tab.error_reported = false
|
|
@@ -779,8 +875,12 @@ module Kward
|
|
|
779
875
|
tab.queued_inputs << input
|
|
780
876
|
@prompt.set_queued_count(tab.queued_inputs.length) if @prompt.respond_to?(:set_queued_count)
|
|
781
877
|
return
|
|
782
|
-
elsif busy_tab_command?(input)
|
|
783
|
-
|
|
878
|
+
elsif busy_tab_command?(input) || busy_name_command?(input)
|
|
879
|
+
if busy_tab_command?(input)
|
|
880
|
+
handle_tab_command(input.to_s.strip.sub(%r{\A/tab(?:\s+|\z)}, ""), @session_store)
|
|
881
|
+
else
|
|
882
|
+
handle_local_slash_command(input, tab.agent, @session_store)
|
|
883
|
+
end
|
|
784
884
|
unless active_tab.equal?(tab)
|
|
785
885
|
@prompt.finish_busy_input if @prompt.respond_to?(:finish_busy_input)
|
|
786
886
|
return :active_tab_changed
|
|
@@ -812,6 +912,10 @@ module Kward
|
|
|
812
912
|
input.to_s.strip.match?(%r{\A/tab(?:\s|\z)})
|
|
813
913
|
end
|
|
814
914
|
|
|
915
|
+
def busy_name_command?(input)
|
|
916
|
+
input.to_s.strip.match?(%r{\A/name(?:\s|\z)})
|
|
917
|
+
end
|
|
918
|
+
|
|
815
919
|
def refresh_active_tab
|
|
816
920
|
tab = active_tab
|
|
817
921
|
return unless tab&.thread && !tab.thread.alive? && tab.running?
|
|
@@ -922,7 +1026,7 @@ module Kward
|
|
|
922
1026
|
return :caution if tab.running? || tab.local_busy?
|
|
923
1027
|
return :failure if %w[failed cancelled].include?(tab.status.to_s) || tab.attention == :failure
|
|
924
1028
|
return :success if tab.attention == :success
|
|
925
|
-
return :
|
|
1029
|
+
return :success if tab.unread
|
|
926
1030
|
|
|
927
1031
|
nil
|
|
928
1032
|
end
|
|
@@ -972,7 +1076,7 @@ module Kward
|
|
|
972
1076
|
end
|
|
973
1077
|
|
|
974
1078
|
def tab_usage
|
|
975
|
-
"/tab 1-n | /tab move 1-n|left|right | /tab close | /tab new | /tab open <plugin-tab> | /tab name <label> | /tab worktree [activate|detach|status|merge|
|
|
1079
|
+
"/tab 1-n | /tab move 1-n|left|right | /tab close | /tab new | /tab open <plugin-tab> | /tab name <label> | /tab worktree [activate|detach|status|merge [resolve|continue|abort]|remove]"
|
|
976
1080
|
end
|
|
977
1081
|
|
|
978
1082
|
def switch_tab_number(number)
|
|
@@ -1042,16 +1146,52 @@ module Kward
|
|
|
1042
1146
|
end
|
|
1043
1147
|
tab.background_run = nil
|
|
1044
1148
|
close_kwsh_session(tab.shell) if tab&.shell && respond_to?(:close_kwsh_session, true)
|
|
1149
|
+
close_plugin_tab(tab)
|
|
1045
1150
|
rescue StandardError
|
|
1046
1151
|
nil
|
|
1047
1152
|
end
|
|
1048
1153
|
end
|
|
1049
1154
|
|
|
1155
|
+
def build_plugin_tab_host(tab_type, workspace_root:, scope_key:)
|
|
1156
|
+
PluginTabHost.new(
|
|
1157
|
+
client: @client,
|
|
1158
|
+
workspace_root: workspace_root,
|
|
1159
|
+
plugin_host: tab_type.plugin_id && plugin_registry.plugin_for(tab_type.plugin_id),
|
|
1160
|
+
type_id: tab_type.id,
|
|
1161
|
+
surface: :local,
|
|
1162
|
+
scope_key: scope_key,
|
|
1163
|
+
capabilities: tab_type.capabilities
|
|
1164
|
+
)
|
|
1165
|
+
end
|
|
1166
|
+
|
|
1167
|
+
def plugin_tab_scope_key(tab_type)
|
|
1168
|
+
tab_type.singleton == :global ? "global" : SecureRandom.uuid
|
|
1169
|
+
end
|
|
1170
|
+
|
|
1171
|
+
def close_plugin_tab(tab)
|
|
1172
|
+
return unless tab&.plugin_host
|
|
1173
|
+
|
|
1174
|
+
if tab.driver.respond_to?(:close)
|
|
1175
|
+
tab.driver.close
|
|
1176
|
+
elsif tab.driver.respond_to?(:shutdown)
|
|
1177
|
+
tab.driver.shutdown
|
|
1178
|
+
end
|
|
1179
|
+
rescue StandardError => e
|
|
1180
|
+
runtime_output("Plugin tab cleanup error: #{e.message}")
|
|
1181
|
+
ensure
|
|
1182
|
+
tab&.plugin_host&.shutdown
|
|
1183
|
+
tab.plugin_host = nil if tab
|
|
1184
|
+
end
|
|
1185
|
+
|
|
1050
1186
|
def persist_tabs
|
|
1051
1187
|
return unless @tab_store
|
|
1052
1188
|
|
|
1053
1189
|
@tab_store.save(
|
|
1054
|
-
tabs: @tabs.map
|
|
1190
|
+
tabs: @tabs.map do |tab|
|
|
1191
|
+
descriptor = tab.driver.descriptor.merge("label" => tab.label.to_s)
|
|
1192
|
+
descriptor["scope_key"] ||= tab.plugin_host.scope_key if tab.plugin_host
|
|
1193
|
+
descriptor
|
|
1194
|
+
end,
|
|
1055
1195
|
active_index: @active_tab_index
|
|
1056
1196
|
)
|
|
1057
1197
|
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
require "base64"
|
|
2
|
-
require_relative "image_attachments"
|
|
3
|
-
require_relative "message_access"
|
|
4
|
-
require_relative "message_text"
|
|
2
|
+
require_relative "../image_attachments"
|
|
3
|
+
require_relative "../message_access"
|
|
4
|
+
require_relative "../message_text"
|
|
5
5
|
|
|
6
6
|
# Namespace for the Kward CLI agent runtime.
|
|
7
7
|
module Kward
|
data/lib/kward/cli/worktrees.rb
CHANGED
|
@@ -101,10 +101,14 @@ module Kward
|
|
|
101
101
|
remove_active_worktree
|
|
102
102
|
when "merge"
|
|
103
103
|
merge_active_worktree
|
|
104
|
+
when "merge resolve"
|
|
105
|
+
resolve_active_worktree_merge
|
|
106
|
+
when "merge continue"
|
|
107
|
+
continue_active_worktree_merge
|
|
104
108
|
when "merge abort"
|
|
105
109
|
abort_active_worktree_merge
|
|
106
110
|
else
|
|
107
|
-
runtime_output("Usage: /tab worktree [activate|detach|status|merge|
|
|
111
|
+
runtime_output("Usage: /tab worktree [activate|detach|status|merge [resolve|continue|abort]|remove]")
|
|
108
112
|
end
|
|
109
113
|
end
|
|
110
114
|
|
|
@@ -222,6 +226,11 @@ module Kward
|
|
|
222
226
|
raise GitWorktreeManager::Error, "Worktree branch changed: expected #{binding.branch}, found #{info.branch || "detached HEAD"}" unless info.branch == binding.branch
|
|
223
227
|
status = git_worktree_manager.status(binding.path)
|
|
224
228
|
lines << "Changes: #{status.clean? ? "clean" : "#{status.entries.length} local change(s)"}"
|
|
229
|
+
if git_worktree_manager.merge_in_progress?(binding.origin_root)
|
|
230
|
+
conflicts = git_worktree_manager.merge_conflict_marker_paths(binding.origin_root, cached: false)
|
|
231
|
+
lines << "Merge: in progress in origin#{conflicts.empty? ? "; ready to continue" : "; #{conflicts.length} unresolved conflict(s)"}"
|
|
232
|
+
lines << "Conflicts: #{conflicts.join(", ")}" unless conflicts.empty?
|
|
233
|
+
end
|
|
225
234
|
rescue GitWorktreeManager::Error => e
|
|
226
235
|
lines << "State: unavailable (#{e.message})"
|
|
227
236
|
end
|
|
@@ -236,6 +245,10 @@ module Kward
|
|
|
236
245
|
return runtime_output("Activate the worktree before merging it.") unless binding.active?
|
|
237
246
|
|
|
238
247
|
validate_worktree_binding!(binding)
|
|
248
|
+
if git_worktree_manager.merge_in_progress?(binding.origin_root)
|
|
249
|
+
return runtime_output("A merge is already in progress in #{binding.origin_root}. Resolve it with /worktree merge resolve, continue it with /worktree merge continue, or abort it with /worktree merge abort.")
|
|
250
|
+
end
|
|
251
|
+
|
|
239
252
|
source_status = git_worktree_manager.status(binding.path)
|
|
240
253
|
return runtime_output("Worktree has local changes; commit or clean it before merging.") if source_status.dirty?
|
|
241
254
|
|
|
@@ -270,12 +283,62 @@ module Kward
|
|
|
270
283
|
if result.merged?
|
|
271
284
|
runtime_output("Merged #{binding.branch} into #{target_branch}.")
|
|
272
285
|
else
|
|
273
|
-
runtime_output(
|
|
286
|
+
runtime_output(<<~MESSAGE.strip)
|
|
287
|
+
Merge conflicts in #{result.conflicts.join(", ")}.
|
|
288
|
+
Merge paused in #{binding.origin_root}; this tab remains on #{binding.branch}.
|
|
289
|
+
Run /worktree merge resolve to let the agent inspect and fix the conflicts here, or /worktree merge abort to cancel.
|
|
290
|
+
MESSAGE
|
|
274
291
|
end
|
|
275
292
|
rescue GitWorktreeManager::Error => e
|
|
276
293
|
runtime_output("Worktree error: #{e.message}")
|
|
277
294
|
end
|
|
278
295
|
|
|
296
|
+
def resolve_active_worktree_merge
|
|
297
|
+
tab = active_tab
|
|
298
|
+
binding = worktree_binding_for(tab)
|
|
299
|
+
return runtime_output("Tab #{active_tab_number} has no worktree binding.") unless binding
|
|
300
|
+
return runtime_output("Tab #{active_tab_number} is running and cannot resolve its worktree merge yet.") if tab.running? || tab.local_busy? || tab.shell
|
|
301
|
+
return runtime_output("Activate the worktree before resolving its merge.") unless binding.active?
|
|
302
|
+
return runtime_output("No merge is in progress in the original workspace.") unless git_worktree_manager.merge_in_progress?(binding.origin_root)
|
|
303
|
+
|
|
304
|
+
validate_worktree_binding!(binding)
|
|
305
|
+
conflicts = git_worktree_manager.unmerged_paths(binding.origin_root)
|
|
306
|
+
return runtime_output("No unresolved conflicts remain in #{binding.origin_root}; run /worktree merge continue to finish the merge.") if conflicts.empty?
|
|
307
|
+
|
|
308
|
+
submit_tab_input(
|
|
309
|
+
tab,
|
|
310
|
+
<<~PROMPT.strip,
|
|
311
|
+
Resolve the in-progress Git merge in the verified origin workspace. Use target: "origin" for every workspace inspection, edit, and test operation. Read the applicable AGENTS.md files first, inspect the unmerged paths, resolve the conflicts, and run the relevant tests. Do not commit the merge; leave it ready for /worktree merge continue after all conflicts are resolved.
|
|
312
|
+
|
|
313
|
+
Unmerged paths: #{conflicts.join(", ")}
|
|
314
|
+
PROMPT
|
|
315
|
+
display_input: "Resolve merge conflicts in origin"
|
|
316
|
+
)
|
|
317
|
+
rescue GitWorktreeManager::Error => e
|
|
318
|
+
runtime_output("Worktree error: #{e.message}")
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def continue_active_worktree_merge
|
|
322
|
+
tab = active_tab
|
|
323
|
+
binding = worktree_binding_for(tab)
|
|
324
|
+
return runtime_output("Tab #{active_tab_number} has no worktree binding.") unless binding
|
|
325
|
+
return runtime_output("Tab #{active_tab_number} is running and cannot continue its worktree merge yet.") if tab.running? || tab.local_busy? || tab.shell
|
|
326
|
+
return runtime_output("Activate the worktree before continuing its merge.") unless binding.active?
|
|
327
|
+
return runtime_output("No merge is in progress in the original workspace.") unless git_worktree_manager.merge_in_progress?(binding.origin_root)
|
|
328
|
+
|
|
329
|
+
validate_worktree_binding!(binding)
|
|
330
|
+
markers = git_worktree_manager.merge_conflict_marker_paths(binding.origin_root, cached: false)
|
|
331
|
+
return runtime_output("Merge still contains conflict markers: #{markers.join(", ")}.") unless markers.empty?
|
|
332
|
+
target_branch = git_worktree_manager.current_branch(binding.origin_root)
|
|
333
|
+
return runtime_output("Original workspace is in detached HEAD state; resolve that before continuing the merge.") if target_branch.empty?
|
|
334
|
+
return unless confirm_worktree_action("Complete the merge of #{binding.branch} into #{target_branch} in #{binding.origin_root}?")
|
|
335
|
+
|
|
336
|
+
git_worktree_manager.continue_merge(binding.origin_root)
|
|
337
|
+
runtime_output("Completed the merge of #{binding.branch} into #{target_branch}.")
|
|
338
|
+
rescue GitWorktreeManager::Error => e
|
|
339
|
+
runtime_output("Worktree error: #{e.message}")
|
|
340
|
+
end
|
|
341
|
+
|
|
279
342
|
def abort_active_worktree_merge
|
|
280
343
|
tab = active_tab
|
|
281
344
|
binding = worktree_binding_for(tab)
|