kward 0.76.0 → 0.77.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 (77) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +8 -1
  3. data/CHANGELOG.md +36 -0
  4. data/Gemfile.lock +2 -2
  5. data/README.md +12 -4
  6. data/doc/agent-tools.md +0 -3
  7. data/doc/api.md +4 -0
  8. data/doc/composer.md +219 -0
  9. data/doc/configuration.md +75 -27
  10. data/doc/editor.md +18 -2
  11. data/doc/files.md +1 -1
  12. data/doc/getting-started.md +4 -0
  13. data/doc/git.md +2 -2
  14. data/doc/lifecycle-hooks.md +2 -14
  15. data/doc/pan.md +167 -0
  16. data/doc/rpc.md +0 -22
  17. data/doc/security.md +180 -0
  18. data/doc/usage.md +15 -32
  19. data/lib/kward/ansi.rb +2 -2
  20. data/lib/kward/cancellation.rb +2 -2
  21. data/lib/kward/cli/commands.rb +1 -3
  22. data/lib/kward/cli/interactive_turn.rb +0 -17
  23. data/lib/kward/cli/plugins.rb +2 -7
  24. data/lib/kward/cli/prompt_interface.rb +1 -0
  25. data/lib/kward/cli/runtime_helpers.rb +0 -38
  26. data/lib/kward/cli/sessions.rb +14 -5
  27. data/lib/kward/cli/settings.rb +22 -0
  28. data/lib/kward/cli/slash_commands.rb +1 -561
  29. data/lib/kward/cli/tabs.rb +14 -2
  30. data/lib/kward/cli.rb +1 -17
  31. data/lib/kward/compactor.rb +3 -3
  32. data/lib/kward/config_files.rb +18 -0
  33. data/lib/kward/ekwsh.rb +0 -4
  34. data/lib/kward/hooks/catalog.rb +0 -5
  35. data/lib/kward/image_attachments.rb +1 -1
  36. data/lib/kward/pan/index.html.erb +459 -64
  37. data/lib/kward/pan/kward_logo.png +0 -0
  38. data/lib/kward/pan/server.rb +248 -23
  39. data/lib/kward/plugin_registry.rb +18 -0
  40. data/lib/kward/prompt_interface/editor/auto_indent.rb +31 -23
  41. data/lib/kward/prompt_interface/editor/modes/vibe.rb +72 -3
  42. data/lib/kward/prompt_interface/layout.rb +0 -4
  43. data/lib/kward/prompt_interface/project_browser.rb +40 -1
  44. data/lib/kward/prompt_interface/runtime_state.rb +4 -0
  45. data/lib/kward/prompt_interface/selection_prompt.rb +0 -1
  46. data/lib/kward/prompt_interface/transcript_renderer.rb +0 -12
  47. data/lib/kward/prompt_interface.rb +18 -1
  48. data/lib/kward/prompts/commands.rb +1 -3
  49. data/lib/kward/prompts.rb +20 -0
  50. data/lib/kward/rpc/server.rb +13 -41
  51. data/lib/kward/rpc/session_manager.rb +75 -25
  52. data/lib/kward/rpc/tool_event_normalizer.rb +2 -0
  53. data/lib/kward/rpc/tool_metadata.rb +23 -0
  54. data/lib/kward/rpc/transcript_normalizer.rb +4 -0
  55. data/lib/kward/session_store.rb +55 -16
  56. data/lib/kward/skills/registry.rb +21 -1
  57. data/lib/kward/tools/base.rb +14 -0
  58. data/lib/kward/tools/registry.rb +11 -16
  59. data/lib/kward/tools/search/code.rb +1 -1
  60. data/lib/kward/tools/search/web.rb +1 -1
  61. data/lib/kward/tools/tool_call.rb +0 -4
  62. data/lib/kward/transcript_export.rb +1 -1
  63. data/lib/kward/version.rb +1 -1
  64. data/templates/default/kward_navigation.rb +5 -1
  65. data/templates/default/layout/html/layout.erb +0 -2
  66. metadata +5 -12
  67. data/lib/kward/workers/git_guard.rb +0 -93
  68. data/lib/kward/workers/job.rb +0 -99
  69. data/lib/kward/workers/live_view.rb +0 -49
  70. data/lib/kward/workers/manager.rb +0 -328
  71. data/lib/kward/workers/queue_runner.rb +0 -166
  72. data/lib/kward/workers/queue_store.rb +0 -112
  73. data/lib/kward/workers/store.rb +0 -72
  74. data/lib/kward/workers/tool_policy.rb +0 -23
  75. data/lib/kward/workers/worker.rb +0 -82
  76. data/lib/kward/workers/write_lock.rb +0 -38
  77. data/lib/kward/workers.rb +0 -10
@@ -32,6 +32,7 @@ module Kward
32
32
 
33
33
  def reload_plugins(conversation)
34
34
  @plugin_registry = PluginRegistry.load(reserved_commands: reserved_slash_command_names)
35
+ @prompt.update_slash_commands(slash_command_entries) if @prompt.respond_to?(:update_slash_commands)
35
36
  conversation.plugin_registry = @plugin_registry if conversation.respond_to?(:plugin_registry=)
36
37
  conversation.refresh_system_message! if conversation.respond_to?(:refresh_system_message!)
37
38
  runtime_output("Plugins reloaded.")
@@ -126,19 +127,13 @@ module Kward
126
127
  end
127
128
 
128
129
  def builtin_slash_commands
129
- return BUILTIN_SLASH_COMMANDS if experimental_workers_enabled?
130
-
131
- BUILTIN_SLASH_COMMANDS.reject { |command| %w[workers queue].include?(command[:name]) }
130
+ BUILTIN_SLASH_COMMANDS
132
131
  end
133
132
 
134
133
  def builtin_slash_command_names
135
134
  builtin_slash_commands.map { |command| command[:name] }
136
135
  end
137
136
 
138
- def experimental_workers_enabled?
139
- @experimental_workers == true
140
- end
141
-
142
137
  def expand_prompt_template(input)
143
138
  PromptCommands.expand(input, templates: prompt_templates, reserved_commands: builtin_slash_command_names)
144
139
  end
@@ -18,6 +18,7 @@ module Kward
18
18
  @prompt = prompt_interface.new(
19
19
  slash_commands: slash_command_entries,
20
20
  overlay_settings: ConfigFiles.overlay_settings,
21
+ project_browser_icon_theme: ConfigFiles.project_browser_icon_theme,
21
22
  footer: prompt_footer_renderer,
22
23
  composer_status: method(:composer_status_text),
23
24
  busy_help: ConfigFiles.composer_busy_help?,
@@ -43,23 +43,13 @@ module Kward
43
43
  end
44
44
 
45
45
  def build_interactive_agent(conversation)
46
- @active_worker_role = "implementation"
47
- set_visible_worker("implementation", status: "active")
48
- build_worker_agent(conversation, role: "implementation")
49
- end
50
-
51
- def build_worker_agent(conversation, role: "implementation")
52
46
  conversation.plugin_registry ||= plugin_registry if conversation.respond_to?(:plugin_registry)
53
47
  workspace = configured_workspace(root: conversation.workspace_root)
54
- writer_id = worker_writer_id(role)
55
48
  hook_manager = lifecycle_hook_manager(conversation)
56
49
  hook_context = lifecycle_hook_context(conversation)
57
50
  tool_registry = ToolRegistry.new(
58
51
  workspace: workspace,
59
52
  prompt: @prompt,
60
- allowed_tool_names: Workers::ToolPolicy.allowed_tool_names(role),
61
- write_lock: @worker_write_lock,
62
- writer_id: writer_id,
63
53
  hook_manager: hook_manager,
64
54
  hook_context: hook_context
65
55
  )
@@ -74,34 +64,6 @@ module Kward
74
64
  )
75
65
  end
76
66
 
77
- def set_visible_worker(id, status: nil, worker: nil)
78
- @visible_worker_id = id.to_s
79
- @visible_worker_status = status
80
- @visible_worker = worker
81
- end
82
-
83
- def worker_writer_id(role)
84
- return nil unless Workers::ToolPolicy.write_capable?(role)
85
-
86
- @worker_write_lock ||= Workers::WriteLock.new
87
- owner_id = role.to_s.empty? ? "implementation" : role.to_s
88
- return owner_id if @worker_write_lock.acquire(owner_id)
89
-
90
- nil
91
- end
92
-
93
- def refresh_implementation_writer(agent)
94
- return agent unless @active_worker_role == "implementation"
95
- return agent unless agent&.respond_to?(:tool_registry)
96
- return agent if agent.tool_registry.writer_id && @worker_write_lock&.owned_by?(agent.tool_registry.writer_id)
97
-
98
- build_interactive_agent(agent.conversation)
99
- end
100
-
101
- def release_implementation_writer
102
- @worker_write_lock&.release("implementation")
103
- end
104
-
105
67
  def handle_interactive_shell_command(input, agent)
106
68
  command = input.to_s.sub(/\A!\s*/, "")
107
69
  if command.strip.empty?
@@ -80,6 +80,15 @@ module Kward
80
80
  @cleanup_sessions.clear
81
81
  end
82
82
 
83
+ def live_session_paths
84
+ (Array(@tabs).filter_map { |tab| tab.session&.path } + [@active_session&.path]).compact.uniq
85
+ end
86
+
87
+ def recent_sessions(session_store, tree: false, limit: nil)
88
+ method = tree ? :recent_tree : :recent
89
+ session_store.public_send(method, limit: limit, keep_empty_path: live_session_paths)
90
+ end
91
+
83
92
  def cleanup_replaced_session(previous_session)
84
93
  return unless previous_session
85
94
  return if @active_session && File.expand_path(previous_session.path) == File.expand_path(@active_session.path)
@@ -590,8 +599,8 @@ module Kward
590
599
  end
591
600
 
592
601
  def insert_session_copy(session_store, sessions, labels, source, copy_path)
593
- copy_info = session_store.recent_tree(limit: nil).find { |session| File.expand_path(session.path) == File.expand_path(copy_path) }
594
- copy_info ||= session_store.recent(limit: nil).find { |session| File.expand_path(session.path) == File.expand_path(copy_path) }
602
+ copy_info = recent_sessions(session_store, tree: true).find { |session| File.expand_path(session.path) == File.expand_path(copy_path) }
603
+ copy_info ||= recent_sessions(session_store).find { |session| File.expand_path(session.path) == File.expand_path(copy_path) }
595
604
  return nil unless copy_info
596
605
 
597
606
  source_index = sessions.index(source) || 0
@@ -618,7 +627,7 @@ module Kward
618
627
  return nil unless source
619
628
 
620
629
  session_store.load(source.path).first.rename(name)
621
- updated = session_store.recent_tree(limit: nil)
630
+ updated = recent_sessions(session_store, tree: true)
622
631
  sessions.replace(updated)
623
632
  labels.replace(session_picker_labels(sessions))
624
633
  index = sessions.index { |session| File.expand_path(session.path) == File.expand_path(source.path) } || 0
@@ -716,7 +725,7 @@ module Kward
716
725
  end
717
726
 
718
727
  def select_session_path(session_store)
719
- select_session_path_from_sessions(session_store.recent_tree(limit: nil), session_store: session_store)
728
+ select_session_path_from_sessions(recent_sessions(session_store, tree: true), session_store: session_store)
720
729
  end
721
730
 
722
731
  def reopen_sessions_after_fork(session_store, source_path, source_label)
@@ -724,7 +733,7 @@ module Kward
724
733
  fork_session_from_picker(session_store, source_path)
725
734
  end
726
735
 
727
- sessions = session_store.recent_tree(limit: nil)
736
+ sessions = recent_sessions(session_store, tree: true)
728
737
  labels = session_picker_labels(sessions)
729
738
  initial_index = if fork_path
730
739
  sessions.index { |session| File.expand_path(session.path) == File.expand_path(fork_path) }
@@ -243,6 +243,8 @@ module Kward
243
243
  next unless width
244
244
 
245
245
  update_overlay_settings("width" => width)
246
+ when /\Afile icons/
247
+ configure_project_browser_icon_theme
246
248
  when /\Ashow busy help/, /\Ahide busy help/
247
249
  set_composer_busy_help(!composer_busy_help?)
248
250
  runtime_output("Busy help #{composer_busy_help? ? "enabled" : "disabled"}. Restart the TUI to apply this setting.")
@@ -277,6 +279,7 @@ module Kward
277
279
  [
278
280
  "Overlay alignment (#{settings["alignment"]})",
279
281
  "Overlay width (#{settings["width"]})",
282
+ "File icons (#{project_browser_icon_theme})",
280
283
  "#{composer_busy_help? ? "Hide" : "Show"} busy help (currently #{on_off(composer_busy_help?)})",
281
284
  "Tab keybindings (#{composer_tab_keybindings})",
282
285
  "Editor mode (#{editor_mode})",
@@ -290,6 +293,25 @@ module Kward
290
293
  ]
291
294
  end
292
295
 
296
+ def project_browser_icon_theme
297
+ ConfigFiles.project_browser_icon_theme(safely_read_config.to_h)
298
+ end
299
+
300
+ def configure_project_browser_icon_theme
301
+ selected = @prompt.select("File icons", project_browser_icon_theme_choices, title: "Settings")
302
+ theme = selected.to_s.split.first.to_s.downcase
303
+ return unless ConfigFiles::PROJECT_BROWSER_ICON_THEMES.include?(theme)
304
+
305
+ update_nested_config("project_browser", "icons" => theme)
306
+ @prompt.update_project_browser_icon_theme(theme) if @prompt.respond_to?(:update_project_browser_icon_theme)
307
+ runtime_output("File icons set to #{theme}.")
308
+ end
309
+
310
+ def project_browser_icon_theme_choices
311
+ current = project_browser_icon_theme
312
+ ConfigFiles::PROJECT_BROWSER_ICON_THEMES.map { |theme| theme == current ? "#{theme} (current)" : theme }
313
+ end
314
+
293
315
  def composer_busy_help?
294
316
  ConfigFiles.composer_busy_help?(safely_read_config.to_h)
295
317
  end