kward 0.75.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 (86) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +8 -1
  3. data/CHANGELOG.md +48 -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 +87 -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/plugins.md +1 -0
  17. data/doc/rpc.md +3 -22
  18. data/doc/security.md +180 -0
  19. data/doc/usage.md +15 -32
  20. data/lib/kward/agent.rb +5 -0
  21. data/lib/kward/ansi.rb +2 -2
  22. data/lib/kward/cancellation.rb +2 -2
  23. data/lib/kward/cli/commands.rb +1 -3
  24. data/lib/kward/cli/interactive_turn.rb +3 -17
  25. data/lib/kward/cli/plugins.rb +2 -7
  26. data/lib/kward/cli/prompt_interface.rb +10 -17
  27. data/lib/kward/cli/rendering.rb +7 -3
  28. data/lib/kward/cli/runtime_helpers.rb +0 -38
  29. data/lib/kward/cli/sessions.rb +14 -5
  30. data/lib/kward/cli/settings.rb +22 -0
  31. data/lib/kward/cli/slash_commands.rb +1 -561
  32. data/lib/kward/cli/tabs.rb +14 -2
  33. data/lib/kward/cli.rb +1 -17
  34. data/lib/kward/cli_transcript_formatter.rb +21 -11
  35. data/lib/kward/compactor.rb +3 -3
  36. data/lib/kward/config_files.rb +21 -0
  37. data/lib/kward/ekwsh.rb +0 -4
  38. data/lib/kward/events.rb +1 -0
  39. data/lib/kward/hooks/catalog.rb +0 -5
  40. data/lib/kward/image_attachments.rb +1 -1
  41. data/lib/kward/model/client.rb +12 -9
  42. data/lib/kward/model/model_info.rb +14 -5
  43. data/lib/kward/model/stream_parser.rb +12 -12
  44. data/lib/kward/pan/index.html.erb +460 -64
  45. data/lib/kward/pan/kward_logo.png +0 -0
  46. data/lib/kward/pan/server.rb +250 -23
  47. data/lib/kward/plugin_registry.rb +20 -0
  48. data/lib/kward/prompt_interface/editor/auto_indent.rb +31 -23
  49. data/lib/kward/prompt_interface/editor/modes/vibe.rb +72 -3
  50. data/lib/kward/prompt_interface/layout.rb +0 -4
  51. data/lib/kward/prompt_interface/project_browser.rb +40 -1
  52. data/lib/kward/prompt_interface/runtime_state.rb +4 -0
  53. data/lib/kward/prompt_interface/selection_prompt.rb +0 -1
  54. data/lib/kward/prompt_interface/transcript_renderer.rb +1 -13
  55. data/lib/kward/prompt_interface.rb +18 -1
  56. data/lib/kward/prompts/commands.rb +1 -3
  57. data/lib/kward/prompts.rb +20 -0
  58. data/lib/kward/rpc/server.rb +14 -42
  59. data/lib/kward/rpc/session_manager.rb +77 -25
  60. data/lib/kward/rpc/tool_event_normalizer.rb +2 -0
  61. data/lib/kward/rpc/tool_metadata.rb +23 -0
  62. data/lib/kward/rpc/transcript_normalizer.rb +14 -10
  63. data/lib/kward/session_store.rb +55 -16
  64. data/lib/kward/skills/registry.rb +21 -1
  65. data/lib/kward/tools/base.rb +14 -0
  66. data/lib/kward/tools/registry.rb +11 -16
  67. data/lib/kward/tools/search/code.rb +1 -1
  68. data/lib/kward/tools/search/web.rb +1 -1
  69. data/lib/kward/tools/tool_call.rb +0 -4
  70. data/lib/kward/transcript_export.rb +1 -1
  71. data/lib/kward/update_check.rb +12 -7
  72. data/lib/kward/version.rb +1 -1
  73. data/templates/default/kward_navigation.rb +5 -1
  74. data/templates/default/layout/html/layout.erb +0 -2
  75. metadata +5 -12
  76. data/lib/kward/workers/git_guard.rb +0 -93
  77. data/lib/kward/workers/job.rb +0 -99
  78. data/lib/kward/workers/live_view.rb +0 -49
  79. data/lib/kward/workers/manager.rb +0 -328
  80. data/lib/kward/workers/queue_runner.rb +0 -166
  81. data/lib/kward/workers/queue_store.rb +0 -112
  82. data/lib/kward/workers/store.rb +0 -72
  83. data/lib/kward/workers/tool_policy.rb +0 -23
  84. data/lib/kward/workers/worker.rb +0 -82
  85. data/lib/kward/workers/write_lock.rb +0 -38
  86. data/lib/kward/workers.rb +0 -10
@@ -452,9 +452,10 @@ module Kward
452
452
  return true if command.match?(/\A\d*g\z/)
453
453
  return true if command.match?(/\A\d*g[uU~]\z/)
454
454
  return true if command.match?(/\A\d*z\z/)
455
- return true if command.match?(/\A\d*[cdy]\d*\z/)
456
- return true if command.match?(/\A\d*[cdy]\d*[ai]\z/)
457
- return true if command.match?(/\A\d*[cdy]\d*[fFtT]\z/)
455
+ return true if command.match?(/\A\d*[=cdy](?:[1-9]\d*)?\z/)
456
+ return true if command.match?(/\A\d*[=cdy]\d*g\z/)
457
+ return true if command.match?(/\A\d*[=cdy]\d*[ai]\z/)
458
+ return true if command.match?(/\A\d*[=cdy]\d*[fFtT]\z/)
458
459
  return true if command.match?(/\A\d*[fFtT]\z/)
459
460
  return true if command.match?(/\A\d*r\z/)
460
461
  return true if command.match?(/\Am\z/)
@@ -635,6 +636,10 @@ module Kward
635
636
  when "<<"
636
637
  vibe_indent_lines(count, :left)
637
638
  vibe_remember_change(command)
639
+ when "=="
640
+ vibe_reindent_lines(count, command)
641
+ when /\A=(.+)\z/
642
+ vibe_reindent_operator(Regexp.last_match(1), count, command)
638
643
  when "p"
639
644
  vibe_active_register_linewise? ? vibe_paste_line(:below, original_command) : vibe_paste_after(original_command)
640
645
  when "P"
@@ -704,6 +709,8 @@ module Kward
704
709
  vibe_indent_visual_selection(:right)
705
710
  when "<"
706
711
  vibe_indent_visual_selection(:left)
712
+ when "="
713
+ vibe_reindent_visual_selection
707
714
  when "J"
708
715
  vibe_join_visual_selection
709
716
  when "~"
@@ -1077,6 +1084,18 @@ module Kward
1077
1084
  vibe_cancel_visual_mode
1078
1085
  end
1079
1086
 
1087
+ def vibe_reindent_visual_selection
1088
+ ranges = @editor_state.selection_ranges
1089
+ return false if ranges.empty?
1090
+
1091
+ start_index = ranges.map(&:first).min
1092
+ end_index = ranges.map(&:last).max
1093
+ start_line, = @editor_state.cursor_line_and_column_for(start_index)
1094
+ end_line, = @editor_state.cursor_line_and_column_for([end_index - 1, start_index].max)
1095
+ vibe_reindent_line_range(start_line, end_line)
1096
+ vibe_cancel_visual_mode
1097
+ end
1098
+
1080
1099
  def vibe_count_and_body(command)
1081
1100
  return [0, "0"] if command == "0"
1082
1101
 
@@ -1284,6 +1303,56 @@ module Kward
1284
1303
  true
1285
1304
  end
1286
1305
 
1306
+ def vibe_reindent_lines(count, command = nil)
1307
+ line, = @editor_state.cursor_line_and_column
1308
+ vibe_reindent_line_range(line, line + count - 1, command)
1309
+ end
1310
+
1311
+ def vibe_reindent_operator(motion, count, command = nil)
1312
+ motion_count, motion = vibe_count_and_body(motion)
1313
+ return vibe_reindent_to_line(motion_count, command) if motion == "G"
1314
+ return vibe_reindent_to_line(motion_count.positive? ? motion_count : 1, command) if motion == "gg"
1315
+ return vibe_reindent_lines(count, command) if motion == "=" || motion == "g_"
1316
+
1317
+ count *= motion_count if motion_count.positive?
1318
+ target = vibe_operator_target(motion, count)
1319
+ return false unless target
1320
+
1321
+ vibe_reindent_target(target, motion, command)
1322
+ end
1323
+
1324
+ def vibe_reindent_to_line(line_count, command)
1325
+ line, = @editor_state.cursor_line_and_column
1326
+ target_line = line_count.positive? ? line_count - 1 : @editor_state.lines.length - 1
1327
+ vibe_reindent_line_range(line, target_line, command)
1328
+ end
1329
+
1330
+ def vibe_reindent_target(target, motion, command)
1331
+ start_line, = @editor_state.cursor_line_and_column_for(target.start_index)
1332
+ end_line = if motion.match?(/\A[ai].\z/)
1333
+ @editor_state.cursor_line_and_column_for([target.end_index - 1, target.start_index].max).first
1334
+ else
1335
+ @editor_state.cursor_line_and_column.first
1336
+ end
1337
+ vibe_reindent_line_range(start_line, end_line, command)
1338
+ end
1339
+
1340
+ def vibe_reindent_line_range(first_line, last_line, command = nil)
1341
+ start_line, end_line = [first_line, last_line].minmax
1342
+ end_line = [end_line, @editor_state.lines.length - 1].min
1343
+ vibe_record_undo do
1344
+ (start_line..end_line).each do |line_index|
1345
+ next if @editor_state.lines[line_index].to_s.strip.empty?
1346
+
1347
+ editor_reindent_line(line_index, editor_expected_indent_for_line(line_index))
1348
+ end
1349
+ end
1350
+ @editor_state.move_to_line_first_non_blank(end_line)
1351
+ @editor_state.status = "Reindented #{end_line - start_line + 1} line#{end_line == start_line ? "" : "s"}"
1352
+ vibe_remember_change(command) if command
1353
+ true
1354
+ end
1355
+
1287
1356
  def vibe_change_lines(count, command = nil)
1288
1357
  start_index, end_index = vibe_linewise_change_range(count)
1289
1358
  @editor_state.copy_range(start_index, end_index)
@@ -10,10 +10,6 @@ module Kward
10
10
  @banner.rows(width, message: message)
11
11
  end
12
12
 
13
- def banner_logo_rows
14
- @banner.logo_rows(screen_width)
15
- end
16
-
17
13
  def transcript_redraw_row_count(height = screen_height)
18
14
  [[@transcript_viewport_rows, transcript_bottom_row(height)].max, height].min
19
15
  end
@@ -12,6 +12,28 @@ module Kward
12
12
  PROJECT_BROWSER_ROOT = "".freeze
13
13
  PROJECT_BROWSER_RESULT_LIMIT = 200
14
14
  PROJECT_BROWSER_STATE_VERSION = 1
15
+ PROJECT_BROWSER_FILENAME_ICONS = {
16
+ ".gitignore" => "",
17
+ "Gemfile" => "",
18
+ "Rakefile" => "",
19
+ "README" => "",
20
+ "README.md" => "",
21
+ "package.json" => ""
22
+ }.freeze
23
+ PROJECT_BROWSER_EXTENSION_ICONS = {
24
+ "css" => "",
25
+ "html" => "",
26
+ "js" => "",
27
+ "json" => "",
28
+ "md" => "",
29
+ "rb" => "",
30
+ "sh" => "",
31
+ "ts" => "",
32
+ "yaml" => "",
33
+ "yml" => ""
34
+ }.freeze
35
+ PROJECT_BROWSER_DIRECTORY_ICON = ""
36
+ PROJECT_BROWSER_FILE_ICON = ""
15
37
 
16
38
  def open_project_browser
17
39
  @mutex.synchronize do
@@ -399,8 +421,25 @@ module Kward
399
421
  else
400
422
  " "
401
423
  end
424
+ icon = project_browser_icon(row)
402
425
  suffix = row[:directory] ? "/" : ""
403
- "#{indent}#{marker}#{row[:name]}#{suffix}"
426
+ "#{indent}#{marker}#{icon}#{row[:name]}#{suffix}"
427
+ end
428
+
429
+ def project_browser_icon(row)
430
+ return "" unless @project_browser_icon_theme == "nerd-font"
431
+
432
+ icon = if row[:directory]
433
+ PROJECT_BROWSER_DIRECTORY_ICON
434
+ else
435
+ project_browser_file_icon(row[:path])
436
+ end
437
+ "#{icon} "
438
+ end
439
+
440
+ def project_browser_file_icon(path)
441
+ name = File.basename(path.to_s)
442
+ PROJECT_BROWSER_FILENAME_ICONS[name] || PROJECT_BROWSER_EXTENSION_ICONS[File.extname(name).delete_prefix(".").downcase] || PROJECT_BROWSER_FILE_ICON
404
443
  end
405
444
 
406
445
  def saved_project_browser_state
@@ -74,6 +74,10 @@ module Kward
74
74
  ANSI.colorize(text, *styles, enabled: @color_enabled)
75
75
  end
76
76
 
77
+ def normalize_project_browser_icon_theme(value)
78
+ value.to_s == "nerd-font" ? "nerd-font" : "off"
79
+ end
80
+
77
81
  def normalize_tab_keybindings(value)
78
82
  text = value.to_s.downcase
79
83
  return "ctrl" if text == "ctrl"
@@ -68,7 +68,6 @@ module Kward
68
68
  return false unless sequence
69
69
 
70
70
  code = sequence[:code]
71
- modifiers = sequence[:modifiers]
72
71
  modifier = sequence[:modifier]
73
72
  queue_pending_keys(sequence[:remaining]) if sequence[:remaining] && !sequence[:remaining].empty?
74
73
 
@@ -15,7 +15,7 @@ module Kward
15
15
  @stream_state.start_block(label)
16
16
  end
17
17
  write_transcript_text_locked(delta) unless delta.empty?
18
- write_transcript_text_locked("\n") if finish && @stream_state.block
18
+ write_transcript_text_locked("\n") if finish && @stream_state.block && !@transcript_buffer.end_with?("\n")
19
19
  @stream_state.finish_block if finish
20
20
  restore_composer_cursor_locked unless @restoring_transcript
21
21
  end
@@ -40,10 +40,6 @@ module Kward
40
40
  @transcript_buffer.append(text.to_s)
41
41
  end
42
42
 
43
- def invalidate_transcript_display_rows_cache
44
- @transcript_buffer.invalidate_display_rows_cache
45
- end
46
-
47
43
  def ensure_transcript_block_separator_locked
48
44
  return if @transcript_buffer.empty? || @transcript_buffer.end_with?("\n\n")
49
45
 
@@ -65,10 +61,6 @@ module Kward
65
61
  @output_io.print(terminal_newlines(rows.join("\n")))
66
62
  end
67
63
 
68
- def transcript_viewport_text(row_count, width)
69
- transcript_viewport_rows(row_count, width).join("\n")
70
- end
71
-
72
64
  def transcript_viewport_rows(row_count, width)
73
65
  return [] unless row_count.positive?
74
66
 
@@ -89,10 +81,6 @@ module Kward
89
81
  @transcript_buffer.display_rows(width)
90
82
  end
91
83
 
92
- def transcript_text_display_rows(width)
93
- @transcript_buffer.text_display_rows(width)
94
- end
95
-
96
84
  def reset_stream_position_from_transcript_locked(width = screen_width)
97
85
  @stream_state.reset_position_from_rows(transcript_display_rows(width), width)
98
86
  end
@@ -126,7 +126,7 @@ module Kward
126
126
  end
127
127
  end
128
128
 
129
- def initialize(input: $stdin, output: $stdout, slash_commands: [], overlay_settings: nil, footer: nil, composer_status: nil, busy_help: true, attachment_badges: nil, attachment_parser: nil, banner_message: nil, tab_keybindings: nil, prompt_history: nil, 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)
129
+ 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, 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)
130
130
  @input_io = input
131
131
  @output_io = output
132
132
  @reader = TTY::Reader.new(input: input, output: output, interrupt: :error)
@@ -186,6 +186,7 @@ module Kward
186
186
  @editor_bar_cursor_active = false
187
187
  @synchronized_output_depth = 0
188
188
  @overlay_settings = normalize_overlay_settings(overlay_settings)
189
+ @project_browser_icon_theme = normalize_project_browser_icon_theme(project_browser_icon_theme)
189
190
  @footer = footer
190
191
  @composer_status = composer_status
191
192
  @busy_help = busy_help
@@ -211,6 +212,15 @@ module Kward
211
212
  @diff_view_source = diff_view_source
212
213
  end
213
214
 
215
+ def update_slash_commands(commands)
216
+ @mutex.synchronize do
217
+ @slash_commands = normalize_slash_commands(commands)
218
+ reset_slash_selection
219
+ @slash_overlay_dismissed_input = nil
220
+ render_prompt_locked if @started
221
+ end
222
+ end
223
+
214
224
  def start(render: true)
215
225
  @mutex.synchronize do
216
226
  return if @started
@@ -661,6 +671,13 @@ module Kward
661
671
  end
662
672
  end
663
673
 
674
+ def update_project_browser_icon_theme(theme)
675
+ @mutex.synchronize do
676
+ @project_browser_icon_theme = normalize_project_browser_icon_theme(theme)
677
+ render_prompt_locked if @started && @asking && project_browser_visible?
678
+ end
679
+ end
680
+
664
681
  def begin_busy_input(message = "You>", activity: "streaming")
665
682
  start
666
683
  @mutex.synchronize do
@@ -19,13 +19,11 @@ module Kward
19
19
  { name: "export", description: "Export the current session as Markdown.", argument_hint: "[path]" },
20
20
  { name: "compact", description: "Compact the current conversation context.", argument_hint: "[instructions]" },
21
21
  { name: "redraw", description: "Refresh the visible terminal.", argument_hint: "" },
22
- { name: "settings", description: "Configure prompt overlays.", argument_hint: "" },
22
+ { name: "settings", description: "Configure Kward interactively.", argument_hint: "" },
23
23
  { name: "login", description: "Log in with an OAuth provider.", argument_hint: "" },
24
24
  { name: "model", description: "Select the default model.", argument_hint: "" },
25
25
  { name: "reasoning", description: "Select reasoning effort.", argument_hint: "" },
26
26
  { name: "reload", description: "Reload installed plugins.", argument_hint: "" },
27
- { name: "workers", description: "Open the worker pipeline.", argument_hint: "[new|do <task>]" },
28
- { name: "queue", description: "Manage the tab-backed worker queue.", argument_hint: "[add|list|open|run|suspend|resume]" },
29
27
  { name: "git", description: "Review uncommitted changes and commit them.", argument_hint: "" },
30
28
  { name: "diff", description: "Open the file changes recorded in this session.", argument_hint: "" },
31
29
  { name: "files", description: "Browse project files.", argument_hint: "" },
data/lib/kward/prompts.rb CHANGED
@@ -3,9 +3,25 @@ require_relative "config_files"
3
3
  # Namespace for the Kward CLI agent runtime.
4
4
  module Kward
5
5
  # System prompt assembly from config, workspace instructions, memory, and plugins.
6
+ #
7
+ # Call {system_message} for the provider-ready message or {prompt_sections}
8
+ # when a frontend needs to inspect the labeled sources separately.
9
+ #
10
+ # @api public
6
11
  module Prompts
7
12
  module_function
8
13
 
14
+ # Builds the system message sent to a model provider.
15
+ #
16
+ # @param workspace_root [String] active workspace root
17
+ # @param include_workspace_personality [Boolean] include persona and plugin context
18
+ # @param model [String, nil] model used for persona matching
19
+ # @param reasoning_effort [String, nil] reasoning effort used for persona matching
20
+ # @param now [Time] time used for persona modifiers
21
+ # @param memory_context [String, nil] retrieved memory text
22
+ # @param plugin_context [String, nil] trusted plugin-provided instructions
23
+ # @return [Hash] role/content system message
24
+ # @api public
9
25
  def system_message(workspace_root: Dir.pwd, include_workspace_personality: true, model: nil, reasoning_effort: nil, now: Time.now, memory_context: nil, plugin_context: nil)
10
26
  {
11
27
  role: "system",
@@ -17,6 +33,10 @@ module Kward
17
33
  prompt_sections(workspace_root: workspace_root, include_workspace_personality: include_workspace_personality, model: model, reasoning_effort: reasoning_effort, now: now, memory_context: memory_context, plugin_context: plugin_context).map { |section| section[:content] }
18
34
  end
19
35
 
36
+ # Returns labeled prompt sections for inspection by CLI and RPC frontends.
37
+ #
38
+ # @return [Array<Hash>] section hashes with label, content, and optional source
39
+ # @api public
20
40
  def prompt_sections(workspace_root: Dir.pwd, include_workspace_personality: true, model: nil, reasoning_effort: nil, now: Time.now, memory_context: nil, plugin_context: nil)
21
41
  sections = [prompt_section("Built-in system prompt", base_prompt)]
22
42
  sections << prompt_section(config_agents_prompt_label, config_agents_prompt, source: config_agents_prompt_source)
@@ -5,7 +5,6 @@ require_relative "../memory/manager"
5
5
  require_relative "../plugin_registry"
6
6
  require_relative "../prompts/commands"
7
7
  require_relative "../tools/registry"
8
- require_relative "../workers"
9
8
  require_relative "../workspace"
10
9
  require_relative "../telemetry/logger"
11
10
  require_relative "../telemetry/stats"
@@ -33,6 +32,7 @@ module Kward
33
32
  # `SessionManager`, `AuthManager`, and `ConfigManager`. When adding an RPC
34
33
  # feature, update dispatch, capabilities, docs, and tests together so clients
35
34
  # can trust `initialize` as the source of supported behavior.
35
+ # @api public
36
36
  class Server
37
37
  PROTOCOL_VERSION = 1
38
38
  JSONRPC_VERSION = "2.0"
@@ -70,7 +70,6 @@ module Kward
70
70
  "memory/forget", "memory/promote", "memory/relax", "memory/inspect",
71
71
  "memory/why", "memory/summarize"
72
72
  ].freeze
73
- WORKER_METHODS = ["workers/list", "workers/show"].freeze
74
73
  COMMAND_METHODS = ["commands/list", "commands/run"].freeze
75
74
  STARTUP_RESOURCE_METHODS = ["resources/startup"].freeze
76
75
  CONFIG_METHODS = ["config/read", "config/update"].freeze
@@ -98,7 +97,6 @@ module Kward
98
97
  runtime_settings: RUNTIME_SETTING_METHODS,
99
98
  auth: AUTH_METHODS,
100
99
  memory: MEMORY_METHODS,
101
- workers: WORKER_METHODS,
102
100
  commands: COMMAND_METHODS,
103
101
  startup_resources: STARTUP_RESOURCE_METHODS,
104
102
  config: CONFIG_METHODS,
@@ -110,15 +108,18 @@ module Kward
110
108
  RPC_METHODS = METHOD_GROUPS.values.flatten.freeze
111
109
 
112
110
  # Creates the RPC server and its stateful managers.
113
- def initialize(input: $stdin, output: $stdout, error_output: $stderr, client: Client.new, experimental_workers: false)
111
+ #
112
+ # @param input [IO] framed JSON-RPC input stream
113
+ # @param output [IO] framed JSON-RPC output stream
114
+ # @param error_output [IO, nil] redacted diagnostic stream
115
+ # @param client [Client] model-provider client shared by sessions
116
+ def initialize(input: $stdin, output: $stdout, error_output: $stderr, client: Client.new)
114
117
  @transport = Transport.new(input: input, output: output)
115
118
  @error_output = error_output
116
119
  @client = client
117
120
  @config_manager = ConfigManager.new
118
121
  @session_manager = SessionManager.new(server: self, client: client, config_manager: @config_manager)
119
122
  @auth_manager = AuthManager.new(server: self, config_manager: @config_manager)
120
- @worker_store = Workers::Store.new
121
- @experimental_workers = experimental_workers
122
123
  @shutdown = false
123
124
  end
124
125
 
@@ -162,6 +163,10 @@ module Kward
162
163
  })
163
164
  end
164
165
 
166
+ # Writes a redacted server diagnostic without exposing error payload data.
167
+ #
168
+ # @param error [Exception]
169
+ # @return [void]
165
170
  def log_error(error)
166
171
  @error_output.puts("Kward RPC error: #{Redactor.redact_string(error.message)}") if @error_output
167
172
  end
@@ -277,12 +282,6 @@ module Kward
277
282
  @session_manager.memory_why(session_id: params["sessionId"])
278
283
  when MEMORY_METHODS[13]
279
284
  @session_manager.memory_summarize(session_id: params.fetch("sessionId"))
280
- when WORKER_METHODS[0]
281
- require_experimental_workers!
282
- workers_list(params)
283
- when WORKER_METHODS[1]
284
- require_experimental_workers!
285
- workers_show(params)
286
285
  when AUTH_METHODS[0]
287
286
  @auth_manager.status
288
287
  when AUTH_METHODS[1]
@@ -426,10 +425,10 @@ module Kward
426
425
  events: {
427
426
  notification: TURN_EVENT_NOTIFICATION,
428
427
  assistantText: "assistantDelta",
429
- reasoning: { start: false, delta: true, end: false },
428
+ reasoning: { start: false, delta: true, boundary: true, end: false },
430
429
  modelRetry: { supported: true, event: "modelRetry" },
431
430
  steering: { supported: @session_manager.in_flight_steer_supported?, event: "turnSteered", mode: @session_manager.in_flight_steer_supported? ? "native" : "unsupported" },
432
- tools: { call: true, update: true, result: true, normalizedMetadata: true, diffs: true, changedFiles: true, workspaceGuardrails: workspace_guardrails_enabled?, focusedContext: true, contextBudgetStats: true },
431
+ tools: { call: true, update: true, result: true, normalizedMetadata: true, diffs: true, firstChangedLine: true, changedFiles: true, workspaceGuardrails: workspace_guardrails_enabled?, focusedContext: true, contextBudgetStats: true },
433
432
  errors: true,
434
433
  sessionUpdates: false
435
434
  },
@@ -484,8 +483,7 @@ module Kward
484
483
  logout: true
485
484
  },
486
485
  memory: { supported: true, optIn: true, defaultEnabled: false, autoSummaryDefaultEnabled: false, promptInjection: "interactive", storage: { core: "json", soft: "jsonl", events: "jsonl" }, methods: MEMORY_METHODS },
487
- workers: workers_capability,
488
- stability: { protocol: "stable", compatibility: "additive-fields-unless-protocol-version-changes", experimentalCapabilities: ["workers"] },
486
+ stability: { protocol: "stable", compatibility: "additive-fields-unless-protocol-version-changes", experimentalCapabilities: [] },
489
487
  commands: { supported: true, methods: COMMAND_METHODS, method: COMMAND_METHODS[0], runMethod: COMMAND_METHODS[1], sources: ["builtin", "prompt", "skill", "plugin"], executableSources: ["builtin", "plugin"] },
490
488
  mcp: {
491
489
  supported: true,
@@ -543,18 +541,6 @@ module Kward
543
541
  }
544
542
  end
545
543
 
546
- def workers_capability
547
- return { supported: false, reason: "experimentalWorkersFlagRequired", flag: "--experimental-workers" } unless @experimental_workers
548
-
549
- { supported: true, methods: WORKER_METHODS, roles: ["implementation", "request"], statuses: Workers::Worker::STATUSES, transcriptStorage: "sessions", metadataStorage: "json" }
550
- end
551
-
552
- def require_experimental_workers!
553
- return if @experimental_workers
554
-
555
- raise NoMethodError, "workers require --experimental-workers"
556
- end
557
-
558
544
  def workspace_info(root)
559
545
  root = @session_manager.validate_workspace_root(root)
560
546
  { root: root, basename: File.basename(root), writable: File.writable?(root) }
@@ -715,20 +701,6 @@ module Kward
715
701
  { sections: sections }
716
702
  end
717
703
 
718
- def workers_list(params)
719
- include_archived = params["includeArchived"] == true
720
- workers = @worker_store.list(include_archived: include_archived)
721
- { workers: workers }
722
- end
723
-
724
- def workers_show(params)
725
- id = params.fetch("id").to_s.delete_prefix("#")
726
- worker = @worker_store.find(id)
727
- return { worker: worker } if worker
728
-
729
- raise ArgumentError, "Unknown worker: #{id}"
730
- end
731
-
732
704
  def auth_login_with_api_key(params)
733
705
  result = @auth_manager.login_with_api_key(provider_id: params.fetch("providerId"), api_key: params.fetch("apiKey"))
734
706
  @session_manager.refresh_client_config