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.
Files changed (193) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +102 -15
  3. data/CONTRIBUTING.md +74 -0
  4. data/Gemfile.lock +8 -2
  5. data/README.md +21 -1
  6. data/Rakefile +46 -2
  7. data/SECURITY.md +31 -0
  8. data/doc/agent-tools.md +13 -1
  9. data/doc/api.md +21 -2
  10. data/doc/composer.md +2 -2
  11. data/doc/configuration.md +95 -25
  12. data/doc/editor.md +28 -13
  13. data/doc/extensibility.md +2 -1
  14. data/doc/files.md +8 -4
  15. data/doc/getting-started.md +3 -0
  16. data/doc/git.md +3 -1
  17. data/doc/pan.md +25 -15
  18. data/doc/permissions.md +4 -4
  19. data/doc/platform-support.md +48 -0
  20. data/doc/plugins.md +464 -15
  21. data/doc/rpc.md +154 -16
  22. data/doc/sandboxing.md +11 -5
  23. data/doc/security.md +10 -3
  24. data/doc/session-management.md +5 -4
  25. data/doc/shell.md +62 -45
  26. data/doc/tabs.md +6 -2
  27. data/doc/transports.md +15 -0
  28. data/doc/troubleshooting.md +12 -2
  29. data/doc/usage.md +9 -6
  30. data/doc/workspace-tools.md +9 -0
  31. data/examples/plugins/space_invaders.rb +1 -1
  32. data/examples/plugins/stardate_footer.rb +2 -2
  33. data/examples/plugins/telegram/plugin.rb +1 -1
  34. data/kward.gemspec +5 -4
  35. data/lib/kward/agent.rb +30 -14
  36. data/lib/kward/cli/auth_commands.rb +34 -13
  37. data/lib/kward/cli/commands.rb +83 -62
  38. data/lib/kward/cli/compaction.rb +9 -3
  39. data/lib/kward/cli/doctor.rb +39 -17
  40. data/lib/kward/cli/hook_commands.rb +22 -12
  41. data/lib/kward/cli/interactive_turn.rb +48 -7
  42. data/lib/kward/cli/plugins.rb +81 -12
  43. data/lib/kward/cli/project_skills_commands.rb +8 -4
  44. data/lib/kward/cli/prompt_interface.rb +52 -5
  45. data/lib/kward/cli/rendering.rb +18 -9
  46. data/lib/kward/cli/runtime_helpers.rb +228 -71
  47. data/lib/kward/cli/sessions.rb +9 -5
  48. data/lib/kward/cli/settings/menus.rb +745 -0
  49. data/lib/kward/cli/settings/model.rb +327 -0
  50. data/lib/kward/cli/settings.rb +6 -1055
  51. data/lib/kward/cli/slash_commands.rb +56 -17
  52. data/lib/kward/cli/tabs.rb +244 -33
  53. data/lib/kward/cli/tool_summaries.rb +14 -0
  54. data/lib/kward/{cli_transcript_formatter.rb → cli/transcript_formatter.rb} +14 -7
  55. data/lib/kward/cli/worktrees.rb +65 -2
  56. data/lib/kward/cli.rb +70 -30
  57. data/lib/kward/compactor.rb +18 -7
  58. data/lib/kward/config/core.rb +389 -0
  59. data/lib/kward/config/extensions.rb +96 -0
  60. data/lib/kward/config/prompts.rb +313 -0
  61. data/lib/kward/config/settings.rb +250 -0
  62. data/lib/kward/config_files.rb +14 -994
  63. data/lib/kward/conversation.rb +31 -2
  64. data/lib/kward/image_attachments.rb +1 -1
  65. data/lib/kward/model/client.rb +36 -24
  66. data/lib/kward/model/copilot_models.rb +2 -2
  67. data/lib/kward/model/model_info.rb +20 -3
  68. data/lib/kward/{openrouter_model_cache.rb → model/openrouter_model_cache.rb} +3 -3
  69. data/lib/kward/model/payloads.rb +12 -3
  70. data/lib/kward/model/provider_catalog.rb +5 -0
  71. data/lib/kward/model/stream_parser.rb +20 -4
  72. data/lib/kward/model/typesafe_client.rb +78 -0
  73. data/lib/kward/pan/index.html.erb +3 -3
  74. data/lib/kward/pan/server.rb +33 -10
  75. data/lib/kward/permissions/policy.rb +6 -2
  76. data/lib/kward/plugin_registry.rb +2 -659
  77. data/lib/kward/plugins/actions.rb +453 -0
  78. data/lib/kward/plugins/chat_contract.rb +121 -0
  79. data/lib/kward/{plugin_chat_runtime.rb → plugins/chat_runtime.rb} +62 -18
  80. data/lib/kward/plugins/host.rb +232 -0
  81. data/lib/kward/plugins/registry.rb +1190 -0
  82. data/lib/kward/plugins/resources.rb +206 -0
  83. data/lib/kward/plugins/turn_request.rb +36 -0
  84. data/lib/kward/plugins/ui.rb +219 -0
  85. data/lib/kward/prompt_interface/composer_renderer.rb +44 -40
  86. data/lib/kward/prompt_interface/composer_state.rb +33 -24
  87. data/lib/kward/prompt_interface/editor/auto_indent.rb +24 -22
  88. data/lib/kward/prompt_interface/editor/controller.rb +30 -33
  89. data/lib/kward/prompt_interface/editor/endwise.rb +13 -4
  90. data/lib/kward/prompt_interface/editor/markdown_code_block.rb +136 -0
  91. data/lib/kward/prompt_interface/editor/modes/vibe.rb +289 -44
  92. data/lib/kward/prompt_interface/editor/renderer.rb +108 -6
  93. data/lib/kward/prompt_interface/editor/runner.rb +362 -0
  94. data/lib/kward/prompt_interface/editor/runner_state.rb +78 -0
  95. data/lib/kward/prompt_interface/editor/scratchpad_languages.rb +74 -0
  96. data/lib/kward/prompt_interface/editor/scratchpad_runner.rb +182 -0
  97. data/lib/kward/prompt_interface/editor/state.rb +11 -11
  98. data/lib/kward/prompt_interface/editor/syntax_highlighter.rb +68 -6
  99. data/lib/kward/prompt_interface/editor/vibe_state.rb +3 -3
  100. data/lib/kward/prompt_interface/file_overlay.rb +71 -15
  101. data/lib/kward/prompt_interface/key_handler.rb +67 -0
  102. data/lib/kward/prompt_interface/layout.rb +1 -1
  103. data/lib/kward/prompt_interface/overlay_renderer.rb +7 -5
  104. data/lib/kward/prompt_interface/plugin_ui_requests.rb +82 -0
  105. data/lib/kward/prompt_interface/project_browser.rb +415 -14
  106. data/lib/kward/prompt_interface/runtime_state.rb +56 -2
  107. data/lib/kward/prompt_interface/screen.rb +11 -4
  108. data/lib/kward/prompt_interface/selection_prompt.rb +3 -1
  109. data/lib/kward/prompt_interface/slash_overlay.rb +19 -4
  110. data/lib/kward/prompt_interface/transcript_renderer.rb +12 -7
  111. data/lib/kward/prompt_interface.rb +151 -27
  112. data/lib/kward/prompts/commands.rb +3 -2
  113. data/lib/kward/prompts.rb +1 -1
  114. data/lib/kward/{adaptive_pty_output_sink.rb → pty/adaptive_output_sink.rb} +1 -1
  115. data/lib/kward/pty/detached_run.rb +44 -0
  116. data/lib/kward/{interactive_pty_runner.rb → pty/interactive_runner.rb} +104 -30
  117. data/lib/kward/{local_command_runner.rb → pty/local_command_runner.rb} +1 -1
  118. data/lib/kward/{local_pty_command_runner.rb → pty/local_pty_runner.rb} +3 -9
  119. data/lib/kward/{pty_output_sink.rb → pty/output_sink.rb} +52 -0
  120. data/lib/kward/{pty_transcript_normalizer.rb → pty/transcript_normalizer.rb} +1 -1
  121. data/lib/kward/rpc/plugin_chat_manager.rb +30 -10
  122. data/lib/kward/rpc/prompt_bridge.rb +25 -0
  123. data/lib/kward/rpc/server.rb +91 -12
  124. data/lib/kward/rpc/session_manager.rb +147 -44
  125. data/lib/kward/rpc/session_tree_rows.rb +2 -2
  126. data/lib/kward/rpc/tool_metadata.rb +1 -1
  127. data/lib/kward/rpc/transcript_normalizer.rb +7 -3
  128. data/lib/kward/sandbox/command_runner.rb +1 -1
  129. data/lib/kward/{session_catalog.rb → sessions/catalog.rb} +1 -1
  130. data/lib/kward/{session_store.rb → sessions/store.rb} +9 -9
  131. data/lib/kward/{session_tree_nodes.rb → sessions/tree_nodes.rb} +3 -3
  132. data/lib/kward/{session_tree_renderer.rb → sessions/tree_renderer.rb} +4 -4
  133. data/lib/kward/{session_tree_tool_display.rb → sessions/tree_tool_display.rb} +1 -1
  134. data/lib/kward/{ekwsh.rb → shell/kwsh.rb} +38 -19
  135. data/lib/kward/shell/kwshrc.rb +233 -0
  136. data/lib/kward/{persistent_shell_session.rb → shell/persistent_session.rb} +121 -28
  137. data/lib/kward/{shell_prompt.rb → shell/prompt.rb} +2 -0
  138. data/lib/kward/{shell_prompt_session.rb → shell/prompt_session.rb} +1 -1
  139. data/lib/kward/skills/trust_store.rb +1 -1
  140. data/lib/kward/tabs/driver.rb +194 -0
  141. data/lib/kward/{tab_store.rb → tabs/store.rb} +2 -2
  142. data/lib/kward/{ansi.rb → terminal/ansi.rb} +110 -10
  143. data/lib/kward/{clipboard.rb → terminal/clipboard.rb} +1 -1
  144. data/lib/kward/{terminal_image_support.rb → terminal/image_support.rb} +1 -1
  145. data/lib/kward/{terminal_keys.rb → terminal/keys.rb} +12 -0
  146. data/lib/kward/terminal/text.rb +121 -0
  147. data/lib/kward/text_matcher.rb +18 -0
  148. data/lib/kward/tools/base.rb +18 -0
  149. data/lib/kward/tools/context_for_task.rb +15 -6
  150. data/lib/kward/tools/edit_file.rb +9 -6
  151. data/lib/kward/tools/git_commit.rb +13 -7
  152. data/lib/kward/tools/list_directory.rb +4 -4
  153. data/lib/kward/tools/open_editor.rb +41 -0
  154. data/lib/kward/tools/plugin_tool.rb +41 -0
  155. data/lib/kward/tools/prepare_shell_command.rb +1 -1
  156. data/lib/kward/tools/read_file.rb +7 -6
  157. data/lib/kward/tools/registry.rb +109 -17
  158. data/lib/kward/tools/run_shell_command.rb +10 -8
  159. data/lib/kward/tools/search/code.rb +1 -1
  160. data/lib/kward/tools/summarize_file_structure.rb +5 -5
  161. data/lib/kward/tools/tool_call.rb +3 -1
  162. data/lib/kward/tools/typesafe_evaluate.rb +81 -0
  163. data/lib/kward/tools/workspace_targets.rb +58 -0
  164. data/lib/kward/tools/write_file.rb +9 -6
  165. data/lib/kward/{export_path.rb → transcripts/export_path.rb} +1 -1
  166. data/lib/kward/{markdown_transcript.rb → transcripts/markdown_transcript.rb} +2 -2
  167. data/lib/kward/transport/contracts.rb +200 -0
  168. data/lib/kward/transport/gateway.rb +79 -35
  169. data/lib/kward/transport/plugin_chat_gateway.rb +3 -2
  170. data/lib/kward/transport.rb +1 -200
  171. data/lib/kward/version.rb +1 -1
  172. data/lib/kward/{workspace_factory.rb → workspace/factory.rb} +2 -2
  173. data/lib/kward/{project_files.rb → workspace/files.rb} +2 -2
  174. data/lib/kward/{git_worktree_manager.rb → workspace/git_worktree_manager.rb} +28 -0
  175. data/lib/kward/{workspace.rb → workspace/workspace.rb} +3 -3
  176. data/templates/default/kward_navigation.rb +1 -0
  177. data/templates/default/layout/html/footer.erb +10 -0
  178. data/templates/default/layout/html/headers.erb +23 -0
  179. data/templates/default/layout/html/layout.erb +2 -2
  180. data/templates/default/layout/html/setup.rb +41 -2
  181. metadata +94 -47
  182. data/lib/kward/scratchpad_runner.rb +0 -56
  183. data/lib/kward/tab_driver.rb +0 -90
  184. /data/lib/kward/{editor_prompt.rb → cli/editor_prompt.rb} +0 -0
  185. /data/lib/kward/{editor_prompt_session.rb → cli/editor_prompt_session.rb} +0 -0
  186. /data/lib/kward/{diff_view_mode.rb → prompt_interface/editor/diff_view_mode.rb} +0 -0
  187. /data/lib/kward/{editor_mode.rb → prompt_interface/editor/editor_mode.rb} +0 -0
  188. /data/lib/kward/{session_diff.rb → sessions/diff.rb} +0 -0
  189. /data/lib/kward/{session_naming.rb → sessions/naming.rb} +0 -0
  190. /data/lib/kward/{session_trash.rb → sessions/trash.rb} +0 -0
  191. /data/lib/kward/{terminal_sequences.rb → terminal/sequences.rb} +0 -0
  192. /data/lib/kward/{transcript_export.rb → transcripts/transcript_export.rb} +0 -0
  193. /data/lib/kward/{path_guard.rb → workspace/path_guard.rb} +0 -0
@@ -6,6 +6,20 @@ module Kward
6
6
  module ToolSummaries
7
7
  private
8
8
 
9
+ def tool_activity(tool_call)
10
+ "running #{tool_call_name(tool_call).tr("_", " ")}"
11
+ end
12
+
13
+ def tool_summary_with_duration(summary, elapsed_ms)
14
+ return summary.to_s unless elapsed_ms
15
+
16
+ lines = summary.to_s.lines
17
+ first = lines.shift.to_s.chomp
18
+ duration = elapsed_ms < 1_000 ? "#{elapsed_ms.round} ms" : format("%.1f s", elapsed_ms / 1_000.0)
19
+ first = "#{first} · #{duration}"
20
+ lines.empty? ? first : (["#{first}\n"] + lines).join
21
+ end
22
+
9
23
  def tool_result_summary(tool_call, content)
10
24
  name = tool_call_name(tool_call)
11
25
  args = tool_call_args(tool_call)
@@ -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
@@ -15,10 +15,17 @@ module Kward
15
15
 
16
16
  def reasoning_blocks(message)
17
17
  response_blocks = response_items(message).flat_map do |item|
18
- next [] unless MessageAccess.value(item, :type) == "reasoning"
19
-
20
- summary = response_item_text_parts(MessageAccess.value(item, :summary))
21
- summary.empty? ? response_item_text_parts(MessageAccess.value(item, :content)) : summary
18
+ case MessageAccess.value(item, :type)
19
+ when "reasoning"
20
+ summary = response_item_text_parts(MessageAccess.value(item, :summary))
21
+ summary.empty? ? response_item_text_parts(MessageAccess.value(item, :content)) : summary
22
+ when "message"
23
+ next [] unless MessageAccess.value(item, :phase).to_s == "commentary"
24
+
25
+ response_item_text_parts(MessageAccess.value(item, :content))
26
+ else
27
+ []
28
+ end
22
29
  end
23
30
  return response_blocks unless response_blocks.empty?
24
31
 
@@ -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|merge abort|remove]")
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("Merge conflicts in #{result.conflicts.join(", ")}. Resolve them in #{binding.origin_root} or run /tab worktree merge abort.")
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)
data/lib/kward/cli.rb CHANGED
@@ -3,26 +3,26 @@ require "json"
3
3
  require "thread"
4
4
  require "tty-prompt"
5
5
  require_relative "agent"
6
- require_relative "ansi"
6
+ require_relative "terminal/ansi"
7
7
  require_relative "version"
8
8
  require_relative "model/client"
9
9
  require_relative "compactor"
10
10
  require_relative "config_files"
11
- require_relative "clipboard"
11
+ require_relative "terminal/clipboard"
12
12
  require_relative "cancellation"
13
- require_relative "editor_prompt"
14
- require_relative "editor_prompt_session"
15
- require_relative "shell_prompt"
16
- require_relative "shell_prompt_session"
17
- require_relative "cli_transcript_formatter"
13
+ require_relative "cli/editor_prompt"
14
+ require_relative "cli/editor_prompt_session"
15
+ require_relative "shell/prompt"
16
+ require_relative "shell/prompt_session"
17
+ require_relative "cli/transcript_formatter"
18
18
  require_relative "model/context_usage"
19
19
  require_relative "events"
20
- require_relative "export_path"
21
- require_relative "ekwsh"
22
- require_relative "persistent_shell_session"
23
- require_relative "interactive_pty_runner"
24
- require_relative "adaptive_pty_output_sink"
25
- require_relative "pty_transcript_normalizer"
20
+ require_relative "transcripts/export_path"
21
+ require_relative "shell/kwsh"
22
+ require_relative "shell/persistent_session"
23
+ require_relative "pty/interactive_runner"
24
+ require_relative "pty/adaptive_output_sink"
25
+ require_relative "pty/transcript_normalizer"
26
26
  require_relative "auth/anthropic_oauth"
27
27
  require_relative "auth/api_key_store"
28
28
  require_relative "auth/github_oauth"
@@ -30,7 +30,7 @@ require_relative "auth/openrouter_api_key"
30
30
  require_relative "image_attachments"
31
31
  require_relative "memory/manager"
32
32
  require_relative "memory/turn_context"
33
- require_relative "transcript_export"
33
+ require_relative "transcripts/transcript_export"
34
34
  require_relative "message_access"
35
35
  require_relative "model/model_info"
36
36
  require_relative "model/azure_openai_config"
@@ -38,18 +38,18 @@ require_relative "model/provider_catalog"
38
38
  require_relative "model/catalog"
39
39
  require_relative "auth/openai_oauth"
40
40
  require_relative "pan/server"
41
- require_relative "plugin_registry"
41
+ require_relative "plugins/registry"
42
42
  require_relative "prompts/commands"
43
43
  require_relative "model/retry_message"
44
44
  require_relative "rpc/server"
45
45
  require_relative "transport/runtime"
46
- require_relative "session_diff"
47
- require_relative "session_store"
48
- require_relative "session_naming"
49
- require_relative "tab_store"
50
- require_relative "tab_driver"
51
- require_relative "session_trash"
52
- require_relative "session_tree_renderer"
46
+ require_relative "sessions/diff"
47
+ require_relative "sessions/store"
48
+ require_relative "sessions/naming"
49
+ require_relative "tabs/store"
50
+ require_relative "tabs/driver"
51
+ require_relative "sessions/trash"
52
+ require_relative "sessions/tree_renderer"
53
53
  require_relative "skills/capture"
54
54
  require_relative "starter_pack_installer"
55
55
  require_relative "steering"
@@ -57,8 +57,8 @@ require_relative "update_check"
57
57
  require_relative "tools/tool_call"
58
58
  require_relative "tools/registry"
59
59
  require_relative "telemetry/stats"
60
- require_relative "workspace_factory"
61
- require_relative "git_worktree_manager"
60
+ require_relative "workspace/factory"
61
+ require_relative "workspace/git_worktree_manager"
62
62
  require_relative "cli/commands"
63
63
  require_relative "cli/auth_commands"
64
64
  require_relative "cli/doctor"
@@ -92,7 +92,7 @@ module Kward
92
92
  RESTORED_TOOL_OUTPUT_LIMIT = 2_000
93
93
  INTERACTIVE_TOOL_OUTPUT_LINE_LIMIT = 10
94
94
  STREAM_RENDER_INTERVAL = 0.025
95
- INTERACTIVE_EVENT_DRAIN_LIMIT = 100
95
+ INTERACTIVE_EVENT_DRAIN_LIMIT = 12
96
96
  BUILTIN_SLASH_COMMANDS = PromptCommands::BUILTIN_COMMANDS
97
97
  BUILTIN_SLASH_COMMAND_NAMES = PromptCommands::BUILTIN_RESERVED_COMMAND_NAMES
98
98
 
@@ -156,7 +156,18 @@ module Kward
156
156
  warn e.message
157
157
  warn "Run `kward help` for available commands."
158
158
  exit 1
159
+ rescue Client::AuthenticationError => e
160
+ raise if debug_errors?
161
+
162
+ warn authentication_error_message(e)
163
+ exit 1
164
+ rescue StandardError => e
165
+ raise if debug_errors?
166
+
167
+ warn runtime_error_message(e)
168
+ exit 1
159
169
  ensure
170
+ shutdown_plugins if respond_to?(:shutdown_plugins, true)
160
171
  ConfigFiles.skip_config = false
161
172
  end
162
173
 
@@ -184,6 +195,30 @@ module Kward
184
195
  MESSAGE
185
196
  end
186
197
 
198
+ def authentication_error_message(error)
199
+ <<~MESSAGE.rstrip
200
+ Authentication required
201
+
202
+ #{error.message}
203
+
204
+ Run `kward doctor` to inspect your setup.
205
+ MESSAGE
206
+ end
207
+
208
+ def runtime_error_message(error)
209
+ <<~MESSAGE.rstrip
210
+ Kward could not complete the request.
211
+
212
+ #{error.message}
213
+
214
+ Set KWARD_DEBUG=1 to show a backtrace.
215
+ MESSAGE
216
+ end
217
+
218
+ def debug_errors?
219
+ ENV["KWARD_DEBUG"] == "1"
220
+ end
221
+
187
222
  def dispatch
188
223
  if @prompt_delimited
189
224
  ConfigFiles.ensure_default_config!
@@ -229,7 +264,7 @@ module Kward
229
264
  raise ArgumentError, command_usage("doctor") unless @argv.length == 1
230
265
 
231
266
  ensure_client!
232
- print_doctor
267
+ exit 1 unless print_doctor
233
268
  return
234
269
  end
235
270
 
@@ -239,7 +274,7 @@ module Kward
239
274
  return
240
275
  end
241
276
 
242
- handle_hooks_command((@argv[1..] || []).join(" "))
277
+ handle_hooks_command((@argv[1..] || []).join(" "), command_line: true)
243
278
  return
244
279
  end
245
280
 
@@ -390,6 +425,7 @@ module Kward
390
425
  tool_registry: ToolRegistry.new(
391
426
  workspace: configured_workspace,
392
427
  prompt: @prompt,
428
+ plugin_tools: plugin_registry.tools,
393
429
  hook_manager: hook_manager,
394
430
  hook_context: hook_context
395
431
  ),
@@ -478,8 +514,9 @@ module Kward
478
514
  install_bang_completion_provider(agent)
479
515
 
480
516
  loop do
481
- if @pending_inputs.empty? && active_tab&.shell
482
- run_ekwsh_loop(active_tab.shell, tab: active_tab, history: build_ekwsh_history(active_tab.agent))
517
+ service_background_tab_runs if respond_to?(:service_background_tab_runs, true)
518
+ if @pending_inputs.empty? && active_tab&.shell && !active_tab.local_busy?
519
+ run_kwsh_loop(active_tab.shell, tab: active_tab, history: build_kwsh_history(active_tab.agent))
483
520
  end
484
521
  input = @pending_inputs.shift || (active_tab ? poll_active_tab_input : @prompt.ask("You>"))
485
522
  if input.is_a?(Hash) && input[:editor_prompt]
@@ -537,7 +574,10 @@ module Kward
537
574
  end
538
575
  end
539
576
  next if handled
540
- next if shell_command_input?(command_input) && handle_interactive_shell_command(command_input, agent)
577
+ if shell_command_input?(command_input)
578
+ shell_result = handle_interactive_shell_command(command_input, agent)
579
+ next if shell_result == true || shell_result == :tab_action
580
+ end
541
581
 
542
582
  flush_pending_reasoning_config(conversation: active_tab.agent.conversation) if active_tab&.agent
543
583
  flush_pending_reasoning_config(conversation: agent.conversation) unless active_tab
@@ -1,5 +1,6 @@
1
1
  require "json"
2
2
  require_relative "model/chat_invocation"
3
+ require_relative "cancellation"
3
4
  require_relative "compaction/file_operation_tracker"
4
5
  require_relative "compaction/token_estimator"
5
6
  require_relative "config_files"
@@ -671,29 +672,33 @@ module Kward
671
672
  @prompt_builder = prompt_builder
672
673
  end
673
674
 
674
- def summarize(preparation, custom_instructions: nil)
675
+ def summarize(preparation, custom_instructions: nil, cancellation: nil)
675
676
  summary = chat(
676
677
  @prompt_builder.build(preparation, custom_instructions: custom_instructions),
677
- max_tokens: @prompt_builder.normal_summary_max_tokens(preparation.settings, model_max_tokens: model_max_tokens)
678
+ max_tokens: @prompt_builder.normal_summary_max_tokens(preparation.settings, model_max_tokens: model_max_tokens),
679
+ cancellation: cancellation
678
680
  )
679
681
  if preparation.split_turn && !preparation.turn_prefix_messages.empty?
680
682
  turn_summary = chat(
681
683
  @prompt_builder.build_split(preparation),
682
- max_tokens: @prompt_builder.split_turn_max_tokens(preparation.settings, model_max_tokens: model_max_tokens)
684
+ max_tokens: @prompt_builder.split_turn_max_tokens(preparation.settings, model_max_tokens: model_max_tokens),
685
+ cancellation: cancellation
683
686
  )
684
687
  summary = "#{summary}\n\n---\n\n**Turn Context (split turn):**\n\n#{turn_summary}"
685
688
  end
686
689
  summary.to_s.strip
687
690
  rescue Cancelled
688
691
  raise
692
+ rescue Kward::Cancellation::CancelledError
693
+ raise
689
694
  rescue StandardError => e
690
695
  raise SummarizationFailed, e.message
691
696
  end
692
697
 
693
698
  private
694
699
 
695
- def chat(messages, max_tokens: nil)
696
- message = ChatInvocation.call(@client, messages, { tools: [], max_tokens: max_tokens })
700
+ def chat(messages, max_tokens: nil, cancellation: nil)
701
+ message = ChatInvocation.call(@client, messages, { tools: [], max_tokens: max_tokens, cancellation: cancellation })
697
702
  content = message_content(message)
698
703
  text = message_content_text(content).strip
699
704
  raise SummarizationFailed, "Compaction produced an empty summary; context was not changed." if text.empty?
@@ -752,13 +757,19 @@ module Kward
752
757
  false
753
758
  end
754
759
 
755
- def compact(custom_instructions: nil, compaction_summary: true)
760
+ def compact(custom_instructions: nil, compaction_summary: true, cancellation: nil)
761
+ cancellation&.raise_if_cancelled!
756
762
  old_count = @conversation.messages.length
757
763
  preparation = prepare
758
- summary = @summarizer.summarize(preparation, custom_instructions: custom_instructions)
764
+ summary = if cancellation
765
+ @summarizer.summarize(preparation, custom_instructions: custom_instructions, cancellation: cancellation)
766
+ else
767
+ @summarizer.summarize(preparation, custom_instructions: custom_instructions)
768
+ end
759
769
  summary = append_files_section(summary, preparation.file_ops)
760
770
  raise Compaction::SummarizationFailed, "Compaction produced an empty summary; context was not changed." if summary.strip.empty?
761
771
 
772
+ cancellation&.raise_if_cancelled!
762
773
  @conversation.compact!(
763
774
  summary,
764
775
  compaction_summary: compaction_summary,