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.
Files changed (155) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +34 -0
  3. data/Gemfile.lock +2 -2
  4. data/README.md +2 -2
  5. data/doc/agent-tools.md +13 -1
  6. data/doc/api.md +17 -2
  7. data/doc/composer.md +1 -1
  8. data/doc/configuration.md +27 -4
  9. data/doc/extensibility.md +2 -1
  10. data/doc/git.md +3 -1
  11. data/doc/pan.md +6 -0
  12. data/doc/permissions.md +4 -4
  13. data/doc/plugins.md +464 -15
  14. data/doc/rpc.md +154 -16
  15. data/doc/sandboxing.md +11 -5
  16. data/doc/security.md +7 -1
  17. data/doc/session-management.md +5 -4
  18. data/doc/tabs.md +6 -2
  19. data/doc/transports.md +15 -0
  20. data/doc/usage.md +4 -1
  21. data/doc/workspace-tools.md +9 -0
  22. data/examples/plugins/space_invaders.rb +1 -1
  23. data/examples/plugins/stardate_footer.rb +2 -2
  24. data/examples/plugins/telegram/plugin.rb +1 -1
  25. data/lib/kward/agent.rb +24 -11
  26. data/lib/kward/cli/compaction.rb +9 -3
  27. data/lib/kward/cli/plugins.rb +81 -12
  28. data/lib/kward/cli/prompt_interface.rb +25 -5
  29. data/lib/kward/cli/rendering.rb +3 -0
  30. data/lib/kward/cli/runtime_helpers.rb +121 -27
  31. data/lib/kward/cli/sessions.rb +9 -5
  32. data/lib/kward/cli/settings/menus.rb +745 -0
  33. data/lib/kward/cli/settings/model.rb +327 -0
  34. data/lib/kward/cli/settings.rb +6 -1055
  35. data/lib/kward/cli/slash_commands.rb +45 -4
  36. data/lib/kward/cli/tabs.rb +167 -27
  37. data/lib/kward/{cli_transcript_formatter.rb → cli/transcript_formatter.rb} +3 -3
  38. data/lib/kward/cli/worktrees.rb +65 -2
  39. data/lib/kward/cli.rb +26 -24
  40. data/lib/kward/compactor.rb +18 -7
  41. data/lib/kward/config/core.rb +389 -0
  42. data/lib/kward/config/extensions.rb +96 -0
  43. data/lib/kward/config/prompts.rb +313 -0
  44. data/lib/kward/config/settings.rb +250 -0
  45. data/lib/kward/config_files.rb +14 -1008
  46. data/lib/kward/conversation.rb +31 -2
  47. data/lib/kward/image_attachments.rb +1 -1
  48. data/lib/kward/model/client.rb +2 -2
  49. data/lib/kward/model/copilot_models.rb +2 -2
  50. data/lib/kward/model/model_info.rb +20 -3
  51. data/lib/kward/{openrouter_model_cache.rb → model/openrouter_model_cache.rb} +3 -3
  52. data/lib/kward/model/payloads.rb +12 -3
  53. data/lib/kward/model/typesafe_client.rb +78 -0
  54. data/lib/kward/pan/server.rb +10 -7
  55. data/lib/kward/permissions/policy.rb +6 -2
  56. data/lib/kward/plugin_registry.rb +2 -659
  57. data/lib/kward/plugins/actions.rb +453 -0
  58. data/lib/kward/plugins/chat_contract.rb +121 -0
  59. data/lib/kward/{plugin_chat_runtime.rb → plugins/chat_runtime.rb} +62 -18
  60. data/lib/kward/plugins/host.rb +232 -0
  61. data/lib/kward/plugins/registry.rb +1190 -0
  62. data/lib/kward/plugins/resources.rb +206 -0
  63. data/lib/kward/plugins/turn_request.rb +36 -0
  64. data/lib/kward/plugins/ui.rb +219 -0
  65. data/lib/kward/prompt_interface/composer_renderer.rb +1 -1
  66. data/lib/kward/prompt_interface/composer_state.rb +1 -1
  67. data/lib/kward/prompt_interface/editor/controller.rb +1 -1
  68. data/lib/kward/prompt_interface/editor/runner.rb +1 -1
  69. data/lib/kward/prompt_interface/editor/state.rb +1 -1
  70. data/lib/kward/prompt_interface/layout.rb +1 -1
  71. data/lib/kward/prompt_interface/overlay_renderer.rb +1 -1
  72. data/lib/kward/prompt_interface/plugin_ui_requests.rb +82 -0
  73. data/lib/kward/prompt_interface/runtime_state.rb +6 -1
  74. data/lib/kward/prompt_interface/screen.rb +9 -2
  75. data/lib/kward/prompt_interface.rb +60 -11
  76. data/lib/kward/prompts/commands.rb +2 -1
  77. data/lib/kward/{adaptive_pty_output_sink.rb → pty/adaptive_output_sink.rb} +1 -1
  78. data/lib/kward/{interactive_pty_runner.rb → pty/interactive_runner.rb} +2 -2
  79. data/lib/kward/{local_command_runner.rb → pty/local_command_runner.rb} +1 -1
  80. data/lib/kward/{local_pty_command_runner.rb → pty/local_pty_runner.rb} +3 -9
  81. data/lib/kward/{pty_output_sink.rb → pty/output_sink.rb} +9 -4
  82. data/lib/kward/{pty_transcript_normalizer.rb → pty/transcript_normalizer.rb} +1 -1
  83. data/lib/kward/rpc/plugin_chat_manager.rb +30 -10
  84. data/lib/kward/rpc/prompt_bridge.rb +25 -0
  85. data/lib/kward/rpc/server.rb +91 -12
  86. data/lib/kward/rpc/session_manager.rb +147 -44
  87. data/lib/kward/rpc/session_tree_rows.rb +2 -2
  88. data/lib/kward/rpc/tool_metadata.rb +1 -1
  89. data/lib/kward/sandbox/command_runner.rb +1 -1
  90. data/lib/kward/{session_catalog.rb → sessions/catalog.rb} +1 -1
  91. data/lib/kward/{session_store.rb → sessions/store.rb} +9 -9
  92. data/lib/kward/{session_tree_nodes.rb → sessions/tree_nodes.rb} +3 -3
  93. data/lib/kward/{session_tree_renderer.rb → sessions/tree_renderer.rb} +4 -4
  94. data/lib/kward/{session_tree_tool_display.rb → sessions/tree_tool_display.rb} +1 -1
  95. data/lib/kward/{kwsh.rb → shell/kwsh.rb} +3 -3
  96. data/lib/kward/{persistent_shell_session.rb → shell/persistent_session.rb} +3 -3
  97. data/lib/kward/{shell_prompt_session.rb → shell/prompt_session.rb} +1 -1
  98. data/lib/kward/skills/trust_store.rb +1 -1
  99. data/lib/kward/tabs/driver.rb +194 -0
  100. data/lib/kward/{tab_store.rb → tabs/store.rb} +2 -2
  101. data/lib/kward/{clipboard.rb → terminal/clipboard.rb} +1 -1
  102. data/lib/kward/{terminal_image_support.rb → terminal/image_support.rb} +1 -1
  103. data/lib/kward/tools/base.rb +18 -0
  104. data/lib/kward/tools/context_for_task.rb +15 -6
  105. data/lib/kward/tools/edit_file.rb +9 -6
  106. data/lib/kward/tools/git_commit.rb +13 -7
  107. data/lib/kward/tools/list_directory.rb +4 -4
  108. data/lib/kward/tools/plugin_tool.rb +41 -0
  109. data/lib/kward/tools/prepare_shell_command.rb +1 -1
  110. data/lib/kward/tools/read_file.rb +7 -6
  111. data/lib/kward/tools/registry.rb +96 -13
  112. data/lib/kward/tools/run_shell_command.rb +10 -8
  113. data/lib/kward/tools/search/code.rb +1 -1
  114. data/lib/kward/tools/summarize_file_structure.rb +5 -5
  115. data/lib/kward/tools/tool_call.rb +2 -1
  116. data/lib/kward/tools/typesafe_evaluate.rb +81 -0
  117. data/lib/kward/tools/workspace_targets.rb +58 -0
  118. data/lib/kward/tools/write_file.rb +9 -6
  119. data/lib/kward/{export_path.rb → transcripts/export_path.rb} +1 -1
  120. data/lib/kward/{markdown_transcript.rb → transcripts/markdown_transcript.rb} +2 -2
  121. data/lib/kward/transport/contracts.rb +200 -0
  122. data/lib/kward/transport/gateway.rb +79 -35
  123. data/lib/kward/transport/plugin_chat_gateway.rb +3 -2
  124. data/lib/kward/transport.rb +1 -200
  125. data/lib/kward/version.rb +1 -1
  126. data/lib/kward/{workspace_factory.rb → workspace/factory.rb} +2 -2
  127. data/lib/kward/{git_worktree_manager.rb → workspace/git_worktree_manager.rb} +28 -0
  128. data/lib/kward/{workspace.rb → workspace/workspace.rb} +3 -3
  129. data/templates/default/fulldoc/html/css/kward.css +125 -0
  130. data/templates/default/fulldoc/html/images/kward_screen_1.png +0 -0
  131. data/templates/default/fulldoc/html/setup.rb +1 -1
  132. data/templates/default/layout/html/layout.erb +16 -4
  133. metadata +67 -48
  134. data/lib/kward/tab_driver.rb +0 -90
  135. data/templates/default/fulldoc/html/images/kward_workflow.svg +0 -52
  136. /data/lib/kward/{editor_prompt.rb → cli/editor_prompt.rb} +0 -0
  137. /data/lib/kward/{editor_prompt_session.rb → cli/editor_prompt_session.rb} +0 -0
  138. /data/lib/kward/{diff_view_mode.rb → prompt_interface/editor/diff_view_mode.rb} +0 -0
  139. /data/lib/kward/{editor_mode.rb → prompt_interface/editor/editor_mode.rb} +0 -0
  140. /data/lib/kward/{markdown_code_block.rb → prompt_interface/editor/markdown_code_block.rb} +0 -0
  141. /data/lib/kward/{scratchpad_languages.rb → prompt_interface/editor/scratchpad_languages.rb} +0 -0
  142. /data/lib/kward/{scratchpad_runner.rb → prompt_interface/editor/scratchpad_runner.rb} +0 -0
  143. /data/lib/kward/{detached_run.rb → pty/detached_run.rb} +0 -0
  144. /data/lib/kward/{session_diff.rb → sessions/diff.rb} +0 -0
  145. /data/lib/kward/{session_naming.rb → sessions/naming.rb} +0 -0
  146. /data/lib/kward/{session_trash.rb → sessions/trash.rb} +0 -0
  147. /data/lib/kward/{kwshrc.rb → shell/kwshrc.rb} +0 -0
  148. /data/lib/kward/{shell_prompt.rb → shell/prompt.rb} +0 -0
  149. /data/lib/kward/{ansi.rb → terminal/ansi.rb} +0 -0
  150. /data/lib/kward/{terminal_keys.rb → terminal/keys.rb} +0 -0
  151. /data/lib/kward/{terminal_sequences.rb → terminal/sequences.rb} +0 -0
  152. /data/lib/kward/{terminal_text.rb → terminal/text.rb} +0 -0
  153. /data/lib/kward/{transcript_export.rb → transcripts/transcript_export.rb} +0 -0
  154. /data/lib/kward/{project_files.rb → workspace/files.rb} +0 -0
  155. /data/lib/kward/{path_guard.rb → workspace/path_guard.rb} +0 -0
@@ -9,24 +9,24 @@ require_relative "../config_files"
9
9
  require_relative "../model/context_usage"
10
10
  require_relative "../conversation"
11
11
  require_relative "../events"
12
- require_relative "../export_path"
12
+ require_relative "../transcripts/export_path"
13
13
  require_relative "../memory/manager"
14
14
  require_relative "../memory/turn_context"
15
15
  require_relative "../message_access"
16
16
  require_relative "../message_text"
17
- require_relative "../session_tree_tool_display"
17
+ require_relative "../sessions/tree_tool_display"
18
18
  require_relative "../skills/capture"
19
19
  require_relative "../model/model_info"
20
- require_relative "../plugin_registry"
20
+ require_relative "../plugins/registry"
21
21
  require_relative "../prompts/commands"
22
- require_relative "../session_store"
23
- require_relative "../session_naming"
24
- require_relative "../session_trash"
22
+ require_relative "../sessions/store"
23
+ require_relative "../sessions/naming"
24
+ require_relative "../sessions/trash"
25
25
  require_relative "../steering"
26
26
  require_relative "../tools/tool_call"
27
27
  require_relative "../tools/registry"
28
- require_relative "../transcript_export"
29
- require_relative "../workspace_factory"
28
+ require_relative "../transcripts/transcript_export"
29
+ require_relative "../workspace/factory"
30
30
  require_relative "attachment_normalizer"
31
31
  require_relative "config_manager"
32
32
  require_relative "memory_methods"
@@ -66,7 +66,7 @@ module Kward
66
66
  WORKER_STOP_TIMEOUT = 2.0
67
67
  WORKER_STOP = Object.new.freeze
68
68
 
69
- RpcSession = Struct.new(:id, :workspace_root, :store, :session, :conversation, :agent, :tool_registry, :execution_profile, :prompt, :plugin_output, :queue, :worker, :running_turn_id, :footer_worker, :last_footer_text, keyword_init: true)
69
+ RpcSession = Struct.new(:id, :workspace_root, :store, :session, :conversation, :agent, :tool_registry, :execution_profile, :prompt, :plugin_output, :queue, :worker, :running_turn_id, :footer_worker, :last_footer_text, :last_footer_segments, keyword_init: true)
70
70
  Turn = Struct.new(:id, :session_id, :input, :display_input, :status, :cancel_requested, :cancellation, :created_at, :started_at, :finished_at, :events, :next_sequence, :error, :streaming_behavior, :plugin_command_name, :plugin_arguments, :steering, :options, :tool_registry, :execution_profile, :mutex, keyword_init: true)
71
71
 
72
72
  # Creates an object for RPC session lifecycle and turn coordination.
@@ -215,7 +215,7 @@ module Kward
215
215
 
216
216
  # Returns the plugin registry shared by RPC sessions and plugin chats.
217
217
  def plugin_registry
218
- @plugin_registry ||= PluginRegistry.load(reserved_commands: reserved_plugin_command_names)
218
+ @plugin_registry ||= PluginRegistry.load(reserved_commands: reserved_plugin_command_names).tap(&:start!)
219
219
  end
220
220
 
221
221
  # Renames the persisted session attached to an RPC session id.
@@ -392,6 +392,11 @@ module Kward
392
392
  { closed: true }
393
393
  end
394
394
 
395
+ def shutdown_plugins
396
+ @plugin_registry&.shutdown!
397
+ { closed: true }
398
+ end
399
+
395
400
  # Returns the normalized transcript for the active RPC session.
396
401
  def transcript(session_id:)
397
402
  rpc_session = fetch_session(session_id)
@@ -490,6 +495,12 @@ module Kward
490
495
  { ok: true }
491
496
  end
492
497
 
498
+ def answer_plugin_ui(session_id:, request_id:, value:)
499
+ rpc_session = fetch_session(session_id)
500
+ rpc_session.prompt.answer_plugin_ui(request_id, value)
501
+ { ok: true }
502
+ end
503
+
493
504
  def answer_tool_approval(session_id:, approval_request_id:, approved:)
494
505
  rpc_session = fetch_session(session_id)
495
506
  rpc_session.prompt.answer_tool_approval(approval_request_id, approved: approved)
@@ -525,18 +536,39 @@ module Kward
525
536
 
526
537
  def run_plugin_command(session_id:, command:, arguments: "")
527
538
  rpc_session = fetch_session(session_id)
539
+ raise ArgumentError, "Plugin commands are disabled for this session" if rpc_session.execution_profile && !rpc_session.execution_profile.plugin_commands
540
+
528
541
  command = plugin_registry.command_for(command.to_s.delete_prefix("/")) || raise(ArgumentError, "Unknown plugin command: #{command}")
542
+ arguments = command.parse_arguments(arguments)
529
543
  output = []
530
- context = plugin_context(rpc_session, args: arguments.to_s, say_callback: lambda { |message| output << message.to_s })
531
- result = command.handler.call(arguments.to_s, context)
544
+ context = plugin_context(rpc_session, args: arguments, say_callback: lambda { |message| output << message.to_s })
545
+ result = command.normalize_result(command.handler.call(arguments, context))
532
546
  output = rpc_session.plugin_output.shift(rpc_session.plugin_output.length) + output
533
- { command: command.name, output: output, result: result.nil? ? nil : result.to_s }
547
+ serialized_result = command.typed? ? result.to_h : (result.nil? ? nil : result.to_s)
548
+ { command: command.name, output: output, result: serialized_result }
534
549
  end
535
550
 
536
551
  def plugin_commands
537
552
  plugin_registry.commands
538
553
  end
539
554
 
555
+ def plugin_actions
556
+ plugin_registry.actions
557
+ end
558
+
559
+ def run_plugin_action(session_id:, id:, arguments: {})
560
+ rpc_session = fetch_session(session_id)
561
+ raise ArgumentError, "Plugin actions are disabled for this session" if rpc_session.execution_profile && !rpc_session.execution_profile.plugin_commands
562
+
563
+ action = plugin_registry.action_for(id) || raise(ArgumentError, "Unknown plugin action: #{id}")
564
+ arguments = action.parse_arguments(arguments)
565
+ output = []
566
+ context = plugin_context(rpc_session, args: arguments, say_callback: lambda { |message| output << message.to_s })
567
+ result = action.normalize_result(action.handler.call(arguments, context))
568
+ output = rpc_session.plugin_output.shift(rpc_session.plugin_output.length) + output
569
+ { action: action.id, output: output, result: result.to_h }
570
+ end
571
+
540
572
  def available_models
541
573
  models = @client.respond_to?(:available_models) ? Array(@client.available_models) : []
542
574
  normalized = models.map { |model| normalize_model(model) }
@@ -628,6 +660,8 @@ module Kward
628
660
 
629
661
  def reload_plugins
630
662
  registry = PluginRegistry.load(reserved_commands: reserved_plugin_command_names)
663
+ @plugin_registry&.reload!
664
+ registry.start!
631
665
  sessions = @mutex.synchronize do
632
666
  @plugin_registry = registry
633
667
  @sessions.values
@@ -636,7 +670,7 @@ module Kward
636
670
  rpc_session.conversation.plugin_registry = registry if rpc_session.conversation.respond_to?(:plugin_registry=)
637
671
  rpc_session.conversation.refresh_system_message! if rpc_session.conversation.respond_to?(:refresh_system_message!)
638
672
  rebuild_session_tools(rpc_session)
639
- if registry.footer_renderer
673
+ if registry.status?
640
674
  start_footer_worker(rpc_session)
641
675
  emit_footer_update(rpc_session)
642
676
  else
@@ -714,7 +748,9 @@ module Kward
714
748
  hook_context = lifecycle_hook_context(
715
749
  conversation: rpc_session.conversation,
716
750
  session: rpc_session.session,
717
- workspace_root: rpc_session.workspace_root
751
+ workspace_root: rpc_session.workspace_root,
752
+ prompt: rpc_session.prompt,
753
+ session_id: rpc_session.id
718
754
  )
719
755
  hook_manager = lifecycle_hook_manager(rpc_session.workspace_root)
720
756
  tool_registry = build_tool_registry(
@@ -1042,7 +1078,13 @@ module Kward
1042
1078
  conversation.plugin_registry ||= plugin_registry if conversation.respond_to?(:plugin_registry)
1043
1079
  id = SecureRandom.uuid
1044
1080
  prompt = PromptBridge.new(notify: method(:notify), session_id: id)
1045
- hook_context = lifecycle_hook_context(conversation: conversation, session: session, workspace_root: workspace_root)
1081
+ hook_context = lifecycle_hook_context(
1082
+ conversation: conversation,
1083
+ session: session,
1084
+ workspace_root: workspace_root,
1085
+ prompt: prompt,
1086
+ session_id: id
1087
+ )
1046
1088
  hook_manager = lifecycle_hook_manager(workspace_root)
1047
1089
  tool_registry = build_tool_registry(workspace_root, prompt, hook_manager: hook_manager, hook_context: hook_context)
1048
1090
  agent = Agent.new(
@@ -1074,6 +1116,7 @@ module Kward
1074
1116
  workspace: configured_workspace(workspace_root),
1075
1117
  prompt: prompt,
1076
1118
  allowed_tool_names: allowed_tool_names,
1119
+ plugin_tools: plugin_registry.tools,
1077
1120
  tool_approval: tool_approval,
1078
1121
  approval_for_allowed_tools: !tool_approval.nil?,
1079
1122
  hook_manager: hook_manager,
@@ -1115,13 +1158,16 @@ module Kward
1115
1158
  })
1116
1159
  end
1117
1160
 
1118
- def lifecycle_hook_context(conversation:, session:, workspace_root:)
1161
+ def lifecycle_hook_context(conversation:, session:, workspace_root:, prompt: nil, session_id: nil)
1162
+ say_callback = lambda { |message| notify("hook/message", { message: message.to_s }) }
1119
1163
  PluginRegistry::Context.new(
1120
1164
  conversation: conversation,
1121
1165
  args: "",
1122
1166
  session: session,
1123
1167
  workspace_root: workspace_root,
1124
- say_callback: lambda { |message| notify("hook/message", { message: message.to_s }) }
1168
+ say_callback: say_callback,
1169
+ ui: rpc_plugin_ui(prompt: prompt, session_id: session_id, say_callback: say_callback, requests: false),
1170
+ tool_ui: rpc_plugin_ui(prompt: prompt, session_id: session_id, say_callback: say_callback, requests: !prompt.nil?)
1125
1171
  )
1126
1172
  end
1127
1173
 
@@ -1212,7 +1258,7 @@ module Kward
1212
1258
  end
1213
1259
 
1214
1260
  def start_footer_worker(rpc_session)
1215
- return unless plugin_registry.footer_renderer
1261
+ return unless plugin_registry.status?
1216
1262
  return if rpc_session.footer_worker&.alive?
1217
1263
 
1218
1264
  rpc_session.footer_worker = Thread.new do
@@ -1290,18 +1336,7 @@ module Kward
1290
1336
  if turn.plugin_command_name
1291
1337
  run_plugin_turn(rpc_session, turn)
1292
1338
  else
1293
- auto_name_session(rpc_session, turn.display_input || turn.input)
1294
- prepare_memory_context(rpc_session.conversation, turn.input) unless memory_disabled?(turn)
1295
- rpc_session.agent.ask(turn.input, display_input: turn_display_input(turn), cancellation: turn.cancellation, steering: turn.steering, options: turn.options || {}, tool_registry: turn.tool_registry) do |event|
1296
- next if turn.cancel_requested
1297
-
1298
- notify_plugin_transcript_event(rpc_session, event)
1299
- handle_agent_event(turn, event)
1300
- end
1301
- unless memory_disabled?(turn)
1302
- persist_memory_state(rpc_session)
1303
- auto_summarize_memory(rpc_session) unless turn.cancel_requested
1304
- end
1339
+ run_session_model_turn(rpc_session, turn, input: turn.input, display_input: turn_display_input(turn))
1305
1340
  finish_turn(turn, turn.cancel_requested ? "canceled" : "completed")
1306
1341
  end
1307
1342
  rescue Cancellation::CancelledError
@@ -1316,6 +1351,22 @@ module Kward
1316
1351
  rpc_session.running_turn_id = nil
1317
1352
  end
1318
1353
 
1354
+ def run_session_model_turn(rpc_session, turn, input:, display_input: nil)
1355
+ auto_name_session(rpc_session, display_input || turn.display_input || input)
1356
+ memory_input = input.is_a?(PluginTurnRequest) ? input.to_s : input
1357
+ prepare_memory_context(rpc_session.conversation, memory_input) unless memory_disabled?(turn)
1358
+ rpc_session.agent.ask(input, display_input: display_input, cancellation: turn.cancellation, steering: turn.steering, options: turn.options || {}, tool_registry: turn.tool_registry) do |event|
1359
+ next if turn.cancel_requested
1360
+
1361
+ notify_plugin_transcript_event(rpc_session, event)
1362
+ handle_agent_event(turn, event)
1363
+ end
1364
+ unless memory_disabled?(turn)
1365
+ persist_memory_state(rpc_session)
1366
+ auto_summarize_memory(rpc_session) unless turn.cancel_requested
1367
+ end
1368
+ end
1369
+
1319
1370
  def build_steering(_turn)
1320
1371
  Steering.new
1321
1372
  end
@@ -1371,12 +1422,44 @@ module Kward
1371
1422
  turn_payload(turn)
1372
1423
  end
1373
1424
 
1374
- def plugin_context(rpc_session, args: nil, say_callback:)
1425
+ def plugin_context(rpc_session, args: nil, say_callback:, requests: false, cancellation: nil, turn_command: nil)
1375
1426
  PluginRegistry::Context.new(
1376
1427
  conversation: rpc_session.conversation,
1377
1428
  args: args,
1378
1429
  session: rpc_session.session,
1379
1430
  workspace_root: rpc_session.workspace_root,
1431
+ say_callback: say_callback,
1432
+ cancellation: cancellation,
1433
+ turn_command: turn_command,
1434
+ ui: rpc_plugin_ui(
1435
+ prompt: rpc_session.prompt,
1436
+ session_id: rpc_session.id,
1437
+ say_callback: say_callback,
1438
+ requests: requests
1439
+ )
1440
+ )
1441
+ end
1442
+
1443
+ def rpc_plugin_ui(prompt:, session_id:, say_callback:, requests:)
1444
+ capabilities = { progress: true, notify: true }
1445
+ requester = nil
1446
+ if requests && prompt
1447
+ capabilities.merge!(question: true, select: true, confirm: true, input: true)
1448
+ requester = lambda do |kind, payload, cancellation: nil|
1449
+ if kind == :question
1450
+ prompt.ask_user_question(payload.fetch(:questions), cancellation: cancellation)
1451
+ else
1452
+ prompt.request_plugin_ui(kind, payload, cancellation: cancellation)
1453
+ end
1454
+ end
1455
+ end
1456
+ emitter = lambda do |kind, payload|
1457
+ method = kind == :progress ? "ui/progress" : "ui/notification"
1458
+ payload = payload.merge(level: payload[:level].to_s) if payload[:level]
1459
+ notify(method, { sessionId: session_id }.merge(payload))
1460
+ end
1461
+ PluginUI.new(
1462
+ backend: PluginUI::Backend.new(capabilities: capabilities, requester: requester, emitter: emitter),
1380
1463
  say_callback: say_callback
1381
1464
  )
1382
1465
  end
@@ -1384,13 +1467,30 @@ module Kward
1384
1467
  def run_plugin_turn(rpc_session, turn)
1385
1468
  turn.cancellation&.raise_if_cancelled!
1386
1469
  command = plugin_registry.command_for(turn.plugin_command_name) || raise(ArgumentError, "Unknown plugin command: #{turn.plugin_command_name}")
1470
+ arguments = command.parse_arguments(turn.plugin_arguments)
1387
1471
  output = []
1388
- context = plugin_context(rpc_session, args: turn.plugin_arguments.to_s, say_callback: lambda { |message| output << message.to_s })
1389
- result = command.handler.call(turn.plugin_arguments.to_s, context)
1472
+ context = plugin_context(
1473
+ rpc_session,
1474
+ args: arguments,
1475
+ say_callback: lambda { |message| output << message.to_s },
1476
+ requests: true,
1477
+ cancellation: turn.cancellation,
1478
+ turn_command: command
1479
+ )
1480
+ result = command.normalize_result(command.handler.call(arguments, context))
1481
+ turn.cancellation&.raise_if_cancelled!
1482
+ if command.typed?
1483
+ emit_turn_event(turn, "pluginCommandResult", { command: command.name, result: result.to_h })
1484
+ result = result.message
1485
+ end
1390
1486
  answer = (output + [result]).compact.map(&:to_s).reject(&:empty?).join("\n")
1391
1487
  unless answer.empty?
1392
1488
  emit_turn_event(turn, "assistantDelta", { delta: answer })
1393
- emit_turn_event(turn, "answer", { content: answer })
1489
+ emit_turn_event(turn, "answer", { content: answer }) unless context.requested_turn
1490
+ end
1491
+ if context.requested_turn
1492
+ turn.steering = build_steering(turn) if supports_in_flight_steer?
1493
+ run_session_model_turn(rpc_session, turn, input: context.requested_turn, display_input: context.requested_turn.to_s)
1394
1494
  end
1395
1495
  finish_turn(turn, turn.cancel_requested ? "canceled" : "completed")
1396
1496
  end
@@ -1457,27 +1557,30 @@ module Kward
1457
1557
  end
1458
1558
 
1459
1559
  def emit_footer_update(rpc_session)
1460
- renderer = plugin_registry.footer_renderer
1461
- return clear_footer_update(rpc_session) unless renderer
1560
+ return clear_footer_update(rpc_session) unless plugin_registry.status?
1462
1561
 
1463
- text = begin
1562
+ segments = begin
1464
1563
  context = plugin_context(rpc_session, say_callback: lambda { |message| rpc_session.plugin_output << message.to_s })
1465
- renderer.call(context).to_s.gsub(/\s+/, " ").strip
1564
+ plugin_registry.status_segments(context)
1466
1565
  rescue StandardError => e
1467
1566
  warn "Warning: Kward plugin footer error: #{e.message}"
1468
- ""
1567
+ []
1469
1568
  end
1470
- return if rpc_session.last_footer_text == text
1569
+ segment_payloads = segments.map(&:to_h)
1570
+ text = plugin_registry.compose_status(segments)
1571
+ return if rpc_session.last_footer_text == text && rpc_session.last_footer_segments == segment_payloads
1471
1572
 
1472
1573
  rpc_session.last_footer_text = text
1473
- notify("ui/footer", { sessionId: rpc_session.id, text: text })
1574
+ rpc_session.last_footer_segments = segment_payloads
1575
+ notify("ui/footer", { sessionId: rpc_session.id, text: text, segments: segment_payloads })
1474
1576
  end
1475
1577
 
1476
1578
  def clear_footer_update(rpc_session)
1477
- return if rpc_session.last_footer_text.to_s.empty?
1579
+ return if rpc_session.last_footer_text.to_s.empty? && Array(rpc_session.last_footer_segments).empty?
1478
1580
 
1479
1581
  rpc_session.last_footer_text = ""
1480
- notify("ui/footer", { sessionId: rpc_session.id, text: "" })
1582
+ rpc_session.last_footer_segments = []
1583
+ notify("ui/footer", { sessionId: rpc_session.id, text: "", segments: [] })
1481
1584
  end
1482
1585
 
1483
1586
  def emit_turn_event(turn, type, payload)
@@ -1,7 +1,7 @@
1
1
  require_relative "../message_access"
2
2
  require_relative "../message_text"
3
- require_relative "../session_tree_nodes"
4
- require_relative "../session_tree_tool_display"
3
+ require_relative "../sessions/tree_nodes"
4
+ require_relative "../sessions/tree_tool_display"
5
5
 
6
6
  # Namespace for the Kward CLI agent runtime.
7
7
  module Kward
@@ -1,5 +1,5 @@
1
1
  require_relative "../tools/tool_call"
2
- require_relative "../workspace"
2
+ require_relative "../workspace/workspace"
3
3
 
4
4
  # Namespace for the Kward CLI agent runtime.
5
5
  module Kward
@@ -1,4 +1,4 @@
1
- require_relative "../local_command_runner"
1
+ require_relative "../pty/local_command_runner"
2
2
 
3
3
  # Namespace for operating-system command sandboxing.
4
4
  module Kward
@@ -1,5 +1,5 @@
1
1
  require "json"
2
- require_relative "private_file"
2
+ require_relative "../private_file"
3
3
 
4
4
  # Namespace for the Kward CLI agent runtime.
5
5
  module Kward
@@ -3,15 +3,15 @@ require "digest"
3
3
  require "json"
4
4
  require "securerandom"
5
5
  require "time"
6
- require_relative "config_files"
7
- require_relative "conversation"
8
- require_relative "message_access"
9
- require_relative "message_text"
10
- require_relative "private_file"
11
- require_relative "rpc/tool_event_normalizer"
12
- require_relative "session_catalog"
13
- require_relative "tools/tool_call"
14
- require_relative "workspace"
6
+ require_relative "../config_files"
7
+ require_relative "../conversation"
8
+ require_relative "../message_access"
9
+ require_relative "../message_text"
10
+ require_relative "../private_file"
11
+ require_relative "../rpc/tool_event_normalizer"
12
+ require_relative "catalog"
13
+ require_relative "../tools/tool_call"
14
+ require_relative "../workspace/workspace"
15
15
 
16
16
  # Namespace for the Kward CLI agent runtime.
17
17
  module Kward
@@ -1,6 +1,6 @@
1
- require_relative "message_access"
2
- require_relative "message_text"
3
- require_relative "tools/tool_call"
1
+ require_relative "../message_access"
2
+ require_relative "../message_text"
3
+ require_relative "../tools/tool_call"
4
4
 
5
5
  # Namespace for the Kward CLI agent runtime.
6
6
  module Kward
@@ -1,7 +1,7 @@
1
- require_relative "message_access"
2
- require_relative "message_text"
3
- require_relative "session_tree_nodes"
4
- require_relative "session_tree_tool_display"
1
+ require_relative "../message_access"
2
+ require_relative "../message_text"
3
+ require_relative "tree_nodes"
4
+ require_relative "tree_tool_display"
5
5
 
6
6
  # Namespace for the Kward CLI agent runtime.
7
7
  module Kward
@@ -1,5 +1,5 @@
1
1
  require "json"
2
- require_relative "tools/tool_call"
2
+ require_relative "../tools/tool_call"
3
3
 
4
4
  # Namespace for the Kward CLI agent runtime.
5
5
  module Kward
@@ -1,8 +1,8 @@
1
1
  require "shellwords"
2
- require_relative "ansi"
3
- require_relative "local_command_runner"
2
+ require_relative "../terminal/ansi"
3
+ require_relative "../pty/local_command_runner"
4
4
  begin
5
- require_relative "local_pty_command_runner"
5
+ require_relative "../pty/local_pty_runner"
6
6
  rescue LoadError
7
7
  nil
8
8
  end
@@ -2,11 +2,11 @@ require "io/console"
2
2
  require "pty"
3
3
  require "securerandom"
4
4
  require "shellwords"
5
- require_relative "ansi"
5
+ require_relative "../terminal/ansi"
6
6
  require_relative "kwsh"
7
7
  require_relative "kwshrc"
8
- require_relative "local_pty_command_runner"
9
- require_relative "detached_run"
8
+ require_relative "../pty/local_pty_runner"
9
+ require_relative "../pty/detached_run"
10
10
 
11
11
  # Namespace for the Kward CLI agent runtime.
12
12
  module Kward
@@ -1,4 +1,4 @@
1
- require_relative "ansi"
1
+ require_relative "../terminal/ansi"
2
2
 
3
3
  # Namespace for the Kward CLI agent runtime.
4
4
  module Kward
@@ -1,7 +1,7 @@
1
1
  require "digest"
2
2
  require "json"
3
3
  require "pathname"
4
- require_relative "../path_guard"
4
+ require_relative "../workspace/path_guard"
5
5
  require_relative "../private_file"
6
6
 
7
7
  # Namespace for Agent Skills discovery and trust management.