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
@@ -3,20 +3,20 @@ require "io/console"
3
3
  require "pathname"
4
4
  require "rbconfig"
5
5
  require "thread"
6
- require_relative "project_files"
6
+ require_relative "workspace/files"
7
7
  require_relative "image_attachments"
8
- require_relative "terminal_image_support"
8
+ require_relative "terminal/image_support"
9
9
  require_relative "prompt_history"
10
10
  require "tty-cursor"
11
11
  require "tty-reader"
12
12
  require "tty-screen"
13
- require_relative "ansi"
14
- require_relative "terminal_sequences"
15
- require_relative "terminal_keys"
16
- require_relative "editor_mode"
17
- require_relative "diff_view_mode"
18
- require_relative "scratchpad_languages"
19
- require_relative "markdown_code_block"
13
+ require_relative "terminal/ansi"
14
+ require_relative "terminal/sequences"
15
+ require_relative "terminal/keys"
16
+ require_relative "prompt_interface/editor/editor_mode"
17
+ require_relative "prompt_interface/editor/diff_view_mode"
18
+ require_relative "prompt_interface/editor/scratchpad_languages"
19
+ require_relative "prompt_interface/editor/markdown_code_block"
20
20
  require_relative "prompt_interface/banner"
21
21
  require_relative "prompt_interface/composer_state"
22
22
  require_relative "prompt_interface/editor/state"
@@ -29,6 +29,7 @@ require_relative "prompt_interface/file_overlay"
29
29
  require_relative "prompt_interface/project_browser"
30
30
  require_relative "prompt_interface/selection_prompt"
31
31
  require_relative "prompt_interface/question_prompt"
32
+ require_relative "prompt_interface/plugin_ui_requests"
32
33
  require_relative "prompt_interface/approval_prompt"
33
34
  require_relative "prompt_interface/git_prompt"
34
35
  require_relative "prompt_interface/overlay_renderer"
@@ -76,6 +77,7 @@ module Kward
76
77
  COMPLETION_DISPLAY_SECONDS = 0.8
77
78
  RESPONSE_ARRIVAL_SECONDS = 0.4
78
79
  BUSY_ELAPSED_DELAY_SECONDS = 2.0
80
+ TRANSCRIPT_COMPOSER_GAP_ROWS = 1
79
81
  COMPOSER_MAX_INPUT_ROWS = 6
80
82
  IMAGE_VIEWER_MAX_ROWS = 8
81
83
  TRANSCRIPT_BUFFER_LIMIT = 200_000
@@ -86,6 +88,7 @@ module Kward
86
88
  include ProjectBrowser
87
89
  include SelectionPrompt
88
90
  include QuestionPrompt
91
+ include PluginUIRequests
89
92
  include ApprovalPrompt
90
93
  include GitPrompt
91
94
  include OverlayRenderer
@@ -140,12 +143,48 @@ module Kward
140
143
  end
141
144
  end
142
145
 
146
+ # Forwards child output while tracking keyboard-protocol resets across chunks.
147
+ class TerminalHandoffOutput
148
+ def initialize(output, on_keyboard_protocol_restore:)
149
+ @output = output
150
+ @on_keyboard_protocol_restore = on_keyboard_protocol_restore
151
+ @pending = +"".b
152
+ end
153
+
154
+ def write(value)
155
+ value = value.to_s.b
156
+ detect_keyboard_protocol_restore(value)
157
+ @output.write(value)
158
+ end
159
+
160
+ def print(*values)
161
+ values.each { |value| write(value) }
162
+ nil
163
+ end
164
+
165
+ def flush
166
+ @output.flush if @output.respond_to?(:flush)
167
+ end
168
+
169
+ private
170
+
171
+ def detect_keyboard_protocol_restore(value)
172
+ @pending << value
173
+ return unless @pending.include?(TerminalSequences::KEYBOARD_PROTOCOL_RESTORE)
174
+
175
+ @on_keyboard_protocol_restore.call
176
+ keep = [TerminalSequences::KEYBOARD_PROTOCOL_RESTORE.bytesize - 1, @pending.bytesize].min
177
+ @pending = keep.positive? ? @pending.byteslice(-keep, keep) : +"".b
178
+ end
179
+ end
180
+
143
181
  def initialize(input: $stdin, output: $stdout, slash_commands: [], overlay_settings: nil, project_browser_icon_theme: "off", footer: nil, composer_status: nil, busy_help: true, attachment_badges: nil, attachment_parser: nil, banner_message: nil, tab_keybindings: nil, prompt_history: nil, workspace_root: Dir.pwd, editor_mode: nil, editor_mode_source: nil, editor_auto_indent: true, editor_auto_indent_source: nil, editor_auto_close_pairs: true, editor_auto_close_pairs_source: nil, editor_soft_wrap: true, editor_soft_wrap_source: nil, editor_bar_cursor: true, editor_bar_cursor_source: nil, editor_line_numbers: "absolute", editor_line_numbers_source: nil, diff_view: "auto", diff_view_source: nil, editor_runners_source: nil, redraw_handler: nil)
144
182
  @input_io = input
145
183
  @output_io = output
146
184
  # Logical state may change during a child handoff, but only the owner may render.
147
185
  @terminal_owner = :kward
148
186
  @terminal_handoff_mode = nil
187
+ @terminal_handoff_keyboard_protocol_reset = false
149
188
  @reader = TTY::Reader.new(input: input, output: output, interrupt: :error)
150
189
  @mutex = Mutex.new
151
190
  @prompt_history = prompt_history
@@ -717,12 +756,19 @@ module Kward
717
756
  print_output_locked(BRACKETED_PASTE_RESTORE)
718
757
  print_output_locked(KEYBOARD_PROTOCOL_RESTORE) unless preserve_tab_keybindings
719
758
  @handoff_input_buffer.clear
759
+ @terminal_handoff_keyboard_protocol_reset = false
720
760
  restore_editor_cursor_shape_locked
721
761
  set_cursor_visible_locked(true, force: true)
722
762
  flush_output_locked
723
763
  restore_console_mode_locked
724
764
  input = @input_io
725
- output = @output_io
765
+ output = if preserve_tab_keybindings
766
+ TerminalHandoffOutput.new(@output_io, on_keyboard_protocol_restore: -> {
767
+ @terminal_handoff_keyboard_protocol_reset = true
768
+ })
769
+ else
770
+ @output_io
771
+ end
726
772
  transition = method(:switch_terminal_handoff_to_exclusive)
727
773
  @terminal_handoff_mode = inline ? :inline : :exclusive
728
774
  @terminal_owner = :child
@@ -736,9 +782,12 @@ module Kward
736
782
  restore_scroll_region_locked
737
783
  print_output_locked(TerminalSequences::SGR_RESET)
738
784
  enter_raw_mode_locked
739
- print_output_locked(KEYBOARD_PROTOCOL_ENABLE)
785
+ if !preserve_tab_keybindings || @terminal_handoff_keyboard_protocol_reset
786
+ print_output_locked(KEYBOARD_PROTOCOL_ENABLE)
787
+ end
740
788
  print_output_locked(BRACKETED_PASTE_ENABLE)
741
789
  @handoff_input_buffer.clear
790
+ @terminal_handoff_keyboard_protocol_reset = false
742
791
  disable_editor_mouse_reporting(force: true)
743
792
  restore_editor_cursor_shape_locked(force: true)
744
793
  @cursor_visible = nil
@@ -11,6 +11,7 @@ module Kward
11
11
  { name: "session", description: "Open saved sessions or name the current session.", argument_hint: "[path]|name [name]" },
12
12
  { name: "resume", description: "Alias for /session.", argument_hint: "[path]" },
13
13
  { name: "rename", description: "Rename the current session.", argument_hint: "<name>" },
14
+ { name: "name", description: "Rename the current session and tab.", argument_hint: "<name>" },
14
15
  { name: "clone", description: "Clone the current session.", argument_hint: "" },
15
16
  { name: "fork", description: "Fork from an earlier prompt into a new session.", argument_hint: "" },
16
17
  { name: "rewind", description: "Revisit an earlier prompt and try a different direction.", argument_hint: "" },
@@ -32,7 +33,7 @@ module Kward
32
33
  { name: "scratchpad", description: "Open an unsaved editor buffer.", argument_hint: "[language|help]" },
33
34
  { name: "pty", description: "Run a command in an interactive PTY passthrough session.", argument_hint: "<command>" },
34
35
  { name: "tab", description: "Manage tabs.", argument_hint: "[1-n|move|close|new|name|worktree]" },
35
- { name: "worktree", description: "Manage the active tab's linked Git worktree.", argument_hint: "[activate|detach|status|merge|merge abort|remove]" },
36
+ { name: "worktree", description: "Manage the active tab's linked Git worktree.", argument_hint: "[activate|detach|status|merge [resolve|continue|abort]|remove]" },
36
37
  { name: "status", description: "Show the current status message.", argument_hint: "" },
37
38
  { name: "stats", description: "Show telemetry logging stats.", argument_hint: "[range]" },
38
39
  { name: "sandbox", description: "Inspect or configure model command sandboxing.", argument_hint: "[status|off|read_only|workspace_write|network allow|network deny]" },
@@ -1,4 +1,4 @@
1
- require_relative "terminal_sequences"
1
+ require_relative "../terminal/sequences"
2
2
 
3
3
  # Namespace for the Kward CLI agent runtime.
4
4
  module Kward
@@ -1,8 +1,8 @@
1
1
  require "io/console"
2
2
  require "pty"
3
3
  require "timeout"
4
- require_relative "local_pty_command_runner"
5
- require_relative "pty_output_sink"
4
+ require_relative "local_pty_runner"
5
+ require_relative "output_sink"
6
6
  require_relative "detached_run"
7
7
 
8
8
  # Namespace for the Kward CLI agent runtime.
@@ -1,7 +1,7 @@
1
1
  require "open3"
2
2
  require "thread"
3
3
  require "timeout"
4
- require_relative "cancellation"
4
+ require_relative "../cancellation"
5
5
 
6
6
  # Namespace for the Kward CLI agent runtime.
7
7
  module Kward
@@ -1,7 +1,7 @@
1
1
  require "io/console"
2
2
  require "pty"
3
3
  require "timeout"
4
- require_relative "cancellation"
4
+ require_relative "../cancellation"
5
5
  require_relative "local_command_runner"
6
6
 
7
7
  # Namespace for the Kward CLI agent runtime.
@@ -52,7 +52,8 @@ module Kward
52
52
  readable, = IO.select([reader], nil, nil, 0.02)
53
53
  next unless readable
54
54
 
55
- chunk = read_chunk(reader)
55
+ chunk = reader.read_nonblock(READ_SIZE, exception: false)
56
+ next if chunk == :wait_readable
56
57
  break if chunk.nil?
57
58
 
58
59
  chunk = normalize_line_endings(chunk)
@@ -102,13 +103,6 @@ module Kward
102
103
  [DEFAULT_ROWS, DEFAULT_COLUMNS]
103
104
  end
104
105
 
105
- def read_chunk(reader)
106
- reader.read_nonblock(READ_SIZE, exception: false).tap do |chunk|
107
- return nil if chunk.nil?
108
- return nil if chunk == :wait_readable
109
- end
110
- end
111
-
112
106
  def normalize_line_endings(chunk)
113
107
  chunk.gsub("\r\r\n", "\n").gsub("\r\n", "\n")
114
108
  end
@@ -4,17 +4,22 @@ require "thread"
4
4
  module Kward
5
5
  # Retains output while a detached PTY command runs without terminal ownership.
6
6
  class BufferedPtyOutputSink
7
- attr_reader :captured_output
8
-
9
- def initialize(max_capture_bytes:)
7
+ def initialize(max_capture_bytes:, initial_output: nil, initial_truncated: false)
10
8
  @max_capture_bytes = max_capture_bytes
11
9
  @captured_output = +"".b
12
- @truncated = false
10
+ @truncated = initial_truncated
13
11
  @mutex = Mutex.new
12
+ write(initial_output) unless initial_output.to_s.empty?
13
+ end
14
+
15
+ def captured_output
16
+ @mutex.synchronize { @captured_output.dup }
14
17
  end
15
18
 
16
19
  def write(chunk)
17
20
  value = chunk.to_s.b
21
+ return if value.empty?
22
+
18
23
  @mutex.synchronize do
19
24
  remaining = @max_capture_bytes - @captured_output.bytesize
20
25
  if value.bytesize > remaining
@@ -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_relative "../cancellation"
2
2
  require_relative "../image_attachments"
3
- require_relative "../plugin_chat_runtime"
4
- require_relative "../plugin_registry"
3
+ require_relative "../plugins/chat_runtime"
4
+ require_relative "../plugins/registry"
5
5
  require_relative "attachment_normalizer"
6
6
  require_relative "transcript_normalizer"
7
7
 
@@ -30,21 +30,25 @@ module Kward
30
30
  @runtime.supported_types(surface: :rpc)
31
31
  end
32
32
 
33
+ def type_entries
34
+ supported_types.map { |type| type_payload(type) }
35
+ end
36
+
33
37
  def list
34
- { chats: supported_types.map { |type| type_payload(type) } }
38
+ { chats: type_entries }
35
39
  end
36
40
 
37
41
  def open(type_id:)
38
42
  chat = @runtime.open(type_id: type_id, surface: :rpc, scope_key: "owner")
39
43
  payload = chat_payload(chat)
40
- return payload if chat.driver.respond_to?(:transcript_page)
44
+ return payload if transcript_paging?(chat)
41
45
 
42
46
  payload.merge(messages: TranscriptNormalizer.new(chat.driver.messages).normalize)
43
47
  end
44
48
 
45
49
  def transcript(chat_id:, limit: nil, before: nil)
46
50
  chat = fetch_chat(chat_id)
47
- page = transcript_page(chat.driver, limit: limit, before: before)
51
+ page = transcript_page(chat, limit: limit, before: before)
48
52
  {
49
53
  chat: chat_payload(chat),
50
54
  messages: TranscriptNormalizer.new(page.fetch(:messages)).normalize,
@@ -70,7 +74,7 @@ module Kward
70
74
  normalized_attachments = AttachmentNormalizer.new.normalize(attachments)
71
75
  turn = @runtime.start_turn(
72
76
  chat_id: chat.id,
73
- input: @runtime.input_with_attachments(input, normalized_attachments),
77
+ input: @runtime.input_with_attachments(input, normalized_attachments, capabilities: chat.type.capabilities),
74
78
  display_input: input.to_s
75
79
  )
76
80
  turn_payload(turn)
@@ -121,19 +125,35 @@ module Kward
121
125
  end
122
126
 
123
127
  def type_payload(type)
124
- {
128
+ payload = {
125
129
  id: type.id,
126
130
  name: type.name,
127
131
  title: type.title,
128
132
  singleton: type.singleton,
129
133
  transport: type.transport == true ? true : nil
130
134
  }.compact
135
+ return payload unless type.capabilities.declared?
136
+
137
+ capabilities = type.capabilities.to_h
138
+ payload.merge(
139
+ apiVersion: capabilities.fetch(:api_version),
140
+ capabilities: {
141
+ attachments: capabilities.fetch(:attachments),
142
+ steering: capabilities.fetch(:steering),
143
+ transcriptPaging: capabilities.fetch(:transcript_paging)
144
+ }
145
+ )
146
+ end
147
+
148
+ def transcript_paging?(chat)
149
+ capabilities = chat.type.capabilities
150
+ capabilities.declared? ? capabilities.transcript_paging? : chat.driver.respond_to?(:transcript_page)
131
151
  end
132
152
 
133
- def transcript_page(driver, limit:, before:)
134
- return { messages: driver.messages, has_more: false } unless limit && driver.respond_to?(:transcript_page)
153
+ def transcript_page(chat, limit:, before:)
154
+ return { messages: chat.driver.messages, has_more: false } unless limit && transcript_paging?(chat)
135
155
 
136
- driver.transcript_page(limit: limit, before: before)
156
+ chat.driver.transcript_page(limit: limit, before: before)
137
157
  end
138
158
 
139
159
  def chat_payload(chat)
@@ -74,6 +74,31 @@ module Kward
74
74
  answer_raw(request_id, normalize_answers(answers))
75
75
  end
76
76
 
77
+ def request_plugin_ui(kind, payload, cancellation: nil)
78
+ request_id = SecureRandom.uuid
79
+ @mutex.synchronize { @pending_requests[request_id] = true }
80
+ cancellation&.on_cancel { cancel_request(request_id) }
81
+ unless cancellation&.cancelled?
82
+ @notify.call("ui/request", {
83
+ sessionId: @session_id,
84
+ requestId: request_id,
85
+ kind: kind.to_s,
86
+ payload: payload
87
+ })
88
+ end
89
+
90
+ @mutex.synchronize do
91
+ @condition.wait(@mutex) until @answers.key?(request_id)
92
+ answer = @answers.delete(request_id)
93
+ @pending_requests.delete(request_id)
94
+ answer
95
+ end
96
+ end
97
+
98
+ def answer_plugin_ui(request_id, value)
99
+ answer_raw(request_id, value)
100
+ end
101
+
77
102
  def cancel_request(request_id)
78
103
  @mutex.synchronize do
79
104
  request_id = request_id.to_s
@@ -2,11 +2,11 @@ require "json"
2
2
  require_relative "../model/client"
3
3
  require_relative "../config_files"
4
4
  require_relative "../memory/manager"
5
- require_relative "../plugin_registry"
5
+ require_relative "../plugins/registry"
6
6
  require_relative "../transport"
7
7
  require_relative "../prompts/commands"
8
8
  require_relative "../tools/registry"
9
- require_relative "../workspace_factory"
9
+ require_relative "../workspace/factory"
10
10
  require_relative "../telemetry/logger"
11
11
  require_relative "../telemetry/stats"
12
12
  require_relative "auth_manager"
@@ -75,18 +75,22 @@ module Kward
75
75
  "memory/why", "memory/summarize"
76
76
  ].freeze
77
77
  COMMAND_METHODS = ["commands/list", "commands/run"].freeze
78
+ PLUGIN_ACTION_METHODS = ["pluginActions/list", "pluginActions/run"].freeze
78
79
  SKILL_CAPTURE_METHODS = ["skills/captureSessions", "skills/captureDraft", "skills/saveCapturedDraft"].freeze
79
80
  STARTUP_RESOURCE_METHODS = ["resources/startup"].freeze
80
81
  CONFIG_METHODS = ["config/read", "config/update"].freeze
81
82
  LOGGING_METHODS = ["logging/stats", "logging/tokenCsv"].freeze
82
83
  LIFECYCLE_HOOK_METHODS = ["hooks/logs"].freeze
83
- UI_METHODS = ["ui/answerQuestion"].freeze
84
+ UI_METHODS = ["ui/answerQuestion", "ui/answerRequest"].freeze
84
85
  TOOL_APPROVAL_METHODS = ["tool/answerApproval"].freeze
85
86
  TRANSPORT_METHODS = ["transports/list", "transports/status"].freeze
86
87
  SESSION_EVENT_NOTIFICATION = "session/event"
87
88
  SESSION_UPDATED_NOTIFICATION = "session/updated"
88
89
  TURN_EVENT_NOTIFICATION = "turn/event"
89
90
  UI_QUESTION_NOTIFICATION = "ui/question"
91
+ UI_REQUEST_NOTIFICATION = "ui/request"
92
+ UI_PROGRESS_NOTIFICATION = "ui/progress"
93
+ UI_NOTIFICATION = "ui/notification"
90
94
  UI_FOOTER_NOTIFICATION = "ui/footer"
91
95
  TOOL_APPROVAL_NOTIFICATION = "tool/approvalRequested"
92
96
  HOOK_EVENT_NOTIFICATION = "hook/event"
@@ -105,6 +109,7 @@ module Kward
105
109
  auth: AUTH_METHODS,
106
110
  memory: MEMORY_METHODS,
107
111
  commands: COMMAND_METHODS,
112
+ plugin_actions: PLUGIN_ACTION_METHODS,
108
113
  skill_capture: SKILL_CAPTURE_METHODS,
109
114
  startup_resources: STARTUP_RESOURCE_METHODS,
110
115
  config: CONFIG_METHODS,
@@ -176,6 +181,7 @@ module Kward
176
181
  @transport_manager.shutdown
177
182
  @plugin_chat_manager.shutdown
178
183
  @session_manager.shutdown_sessions
184
+ @session_manager.shutdown_plugins
179
185
  @shutdown_complete = true
180
186
  end
181
187
 
@@ -284,6 +290,10 @@ module Kward
284
290
  commands_list(params)
285
291
  when COMMAND_METHODS[1]
286
292
  commands_run(params)
293
+ when PLUGIN_ACTION_METHODS[0]
294
+ plugin_actions_list(params)
295
+ when PLUGIN_ACTION_METHODS[1]
296
+ plugin_actions_run(params)
287
297
  when SKILL_CAPTURE_METHODS[0]
288
298
  { sessions: @session_manager.skill_capture_sessions }
289
299
  when SKILL_CAPTURE_METHODS[1]
@@ -421,6 +431,8 @@ module Kward
421
431
  @plugin_chat_manager.list_turns(chat_id: params["chatId"], active: true)
422
432
  when UI_METHODS[0]
423
433
  @session_manager.answer_question(session_id: params.fetch("sessionId"), question_request_id: params.fetch("questionRequestId"), answers: params.fetch("answers"))
434
+ when UI_METHODS[1]
435
+ @session_manager.answer_plugin_ui(session_id: params.fetch("sessionId"), request_id: params.fetch("requestId"), value: params["value"])
424
436
  when TOOL_APPROVAL_METHODS[0]
425
437
  @session_manager.answer_tool_approval(session_id: params.fetch("sessionId"), approval_request_id: params.fetch("approvalRequestId"), approved: params.fetch("approved"))
426
438
  else
@@ -469,13 +481,27 @@ module Kward
469
481
  startMode: "cliOnly",
470
482
  entries: @transport_manager.list
471
483
  },
484
+ plugins: {
485
+ apiVersion: PluginRegistry::PLUGIN_API_VERSION,
486
+ identified: @session_manager.plugin_registry.plugins.map do |plugin|
487
+ { id: plugin.id, version: plugin.version, apiVersion: plugin.api_version }
488
+ end
489
+ },
490
+ pluginTools: {
491
+ supported: true,
492
+ registered: @session_manager.plugin_registry.tools.length,
493
+ discoveryMethod: TOOL_METHODS.first,
494
+ source: "plugin",
495
+ executionProfileFiltering: true,
496
+ permissionPolicy: true
497
+ },
472
498
  pluginChats: {
473
499
  supported: @plugin_chat_manager.supported_types.any?,
474
500
  methods: PLUGIN_CHAT_METHODS,
475
501
  notification: "pluginChat/event",
476
502
  subscriptions: { supported: true, methods: PLUGIN_CHAT_METHODS.values_at(3, 4), requiredForLiveEvents: true },
477
503
  attachments: { supported: true, method: PLUGIN_CHAT_METHODS[5], encoding: "base64", mimeTypes: SessionManager::RPC_IMAGE_MIME_TYPES, maxBytes: SessionManager::RPC_ATTACHMENT_MAX_BYTES },
478
- types: @plugin_chat_manager.supported_types.map { |type| { id: type.id, name: type.name, title: type.title, singleton: type.singleton, transport: type.transport == true ? true : nil }.compact }
504
+ types: @plugin_chat_manager.type_entries
479
505
  },
480
506
  turns: {
481
507
  mode: "async",
@@ -513,6 +539,7 @@ module Kward
513
539
  reasoning: { start: false, delta: true, boundary: true, end: false },
514
540
  modelRetry: { supported: true, event: "modelRetry" },
515
541
  steering: { supported: @session_manager.in_flight_steer_supported?, event: "turnSteered", mode: @session_manager.in_flight_steer_supported? ? "native" : "unsupported" },
542
+ pluginCommands: { structuredResult: "pluginCommandResult" },
516
543
  tools: { call: true, update: true, result: true, normalizedMetadata: true, diffs: true, firstChangedLine: true, changedFiles: true, workspaceGuardrails: workspace_guardrails_enabled?, focusedContext: true, contextBudgetStats: true },
517
544
  errors: true,
518
545
  sessionUpdates: false
@@ -577,7 +604,31 @@ module Kward
577
604
  },
578
605
  memory: { supported: true, optIn: true, defaultEnabled: false, autoSummaryDefaultEnabled: false, promptInjection: "interactive", storage: { core: "json", soft: "jsonl", events: "jsonl" }, methods: MEMORY_METHODS },
579
606
  stability: { protocol: "stable", compatibility: "additive-fields-unless-protocol-version-changes", experimentalCapabilities: [] },
580
- commands: { supported: true, methods: COMMAND_METHODS, method: COMMAND_METHODS[0], runMethod: COMMAND_METHODS[1], sources: ["builtin", "prompt", "skill", "plugin"], executableSources: ["builtin", "plugin"] },
607
+ commands: {
608
+ supported: true,
609
+ methods: COMMAND_METHODS,
610
+ method: COMMAND_METHODS[0],
611
+ runMethod: COMMAND_METHODS[1],
612
+ sources: ["builtin", "prompt", "skill", "plugin"],
613
+ executableSources: ["builtin", "plugin"],
614
+ typedArguments: { supported: true, schema: "jsonSchemaObject", textSyntax: "shellFlags", legacyRawStrings: true },
615
+ structuredResults: true,
616
+ modelTurns: {
617
+ supported: true, method: "turns/start", synchronous: false,
618
+ inputRole: "system", codexInputRole: "developer", scope: "turn", maxSystemBytes: PluginTurnRequest::MAX_SYSTEM_BYTES,
619
+ pluginChats: false, pan: false
620
+ }
621
+ },
622
+ pluginActions: {
623
+ supported: true,
624
+ methods: PLUGIN_ACTION_METHODS,
625
+ registered: @session_manager.plugin_actions.length,
626
+ namespace: "pluginId/actionName",
627
+ arguments: "jsonSchemaObject",
628
+ structuredResults: true,
629
+ blockingUi: false,
630
+ localTui: false
631
+ },
581
632
  skillCapture: { supported: true, methods: SKILL_CAPTURE_METHODS, destination: "personal", source: "savedSessionActiveLeaf", reviewRequired: true, overwrite: "explicit", autoActivate: false },
582
633
  projectSkillTrust: { supported: false, trustRequired: true, reason: "RPC has no interactive trust decision bridge; project skills remain skipped unless globally enabled." },
583
634
  mcp: {
@@ -599,12 +650,14 @@ module Kward
599
650
  scratchpad: { supported: false, reason: "interactiveTuiOnly" },
600
651
  extensionUi: {
601
652
  question: { supported: true, notification: UI_QUESTION_NOTIFICATION, method: UI_METHODS.first, maxQuestions: 4, multiSelect: false, preview: false },
602
- select: false,
603
- confirm: false,
604
- input: false,
653
+ select: { supported: true, notification: UI_REQUEST_NOTIFICATION, method: UI_METHODS[1], maxOptions: PluginUI::MAX_OPTIONS },
654
+ confirm: { supported: true, notification: UI_REQUEST_NOTIFICATION, method: UI_METHODS[1] },
655
+ input: { supported: true, notification: UI_REQUEST_NOTIFICATION, method: UI_METHODS[1], maxBytes: PluginUI::MAX_TEXT_BYTES },
656
+ progress: { supported: true, notification: UI_PROGRESS_NOTIFICATION },
657
+ notify: { supported: true, notification: UI_NOTIFICATION, levels: PluginUI::NOTIFICATION_LEVELS.map(&:to_s) },
605
658
  editor: false,
606
659
  widgets: false,
607
- footer: { supported: true, notification: UI_FOOTER_NOTIFICATION },
660
+ footer: { supported: true, notification: UI_FOOTER_NOTIFICATION, segments: true, tooltip: true, priorities: PluginRegistry::STATUS_PRIORITIES.keys.map(&:to_s) },
608
661
  custom: false,
609
662
  terminalInput: false
610
663
  },
@@ -789,8 +842,12 @@ module Kward
789
842
  argumentHint: command.argument_hint,
790
843
  source: "plugin",
791
844
  path: command.path,
792
- executable: true
793
- }
845
+ pluginId: command.plugin_id,
846
+ executable: true,
847
+ typed: command.typed?,
848
+ schema: command.schema,
849
+ positionals: command.positionals
850
+ }.compact
794
851
  end
795
852
  { commands: builtins + prompts + skills + plugins }
796
853
  end
@@ -799,7 +856,29 @@ module Kward
799
856
  @session_manager.run_command(
800
857
  session_id: params.fetch("sessionId"),
801
858
  command: params.fetch("name"),
802
- arguments: params["arguments"] || ""
859
+ arguments: params.key?("arguments") ? params["arguments"] : ""
860
+ )
861
+ end
862
+
863
+ def plugin_actions_list(params)
864
+ @session_manager.runtime_state(session_id: params.fetch("sessionId"))
865
+ actions = @session_manager.plugin_actions.map do |action|
866
+ {
867
+ id: action.id,
868
+ name: action.name,
869
+ pluginId: action.plugin_id,
870
+ description: action.description,
871
+ schema: action.schema
872
+ }
873
+ end
874
+ { actions: actions }
875
+ end
876
+
877
+ def plugin_actions_run(params)
878
+ @session_manager.run_plugin_action(
879
+ session_id: params.fetch("sessionId"),
880
+ id: params.fetch("id"),
881
+ arguments: params.key?("arguments") ? params["arguments"] : {}
803
882
  )
804
883
  end
805
884