kward 0.82.0 → 0.84.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 (115) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +96 -15
  3. data/CONTRIBUTING.md +74 -0
  4. data/Gemfile.lock +8 -2
  5. data/README.md +21 -1
  6. data/Rakefile +46 -2
  7. data/SECURITY.md +31 -0
  8. data/doc/agent-tools.md +1 -0
  9. data/doc/api.md +4 -0
  10. data/doc/composer.md +2 -2
  11. data/doc/configuration.md +82 -20
  12. data/doc/editor.md +47 -22
  13. data/doc/files.md +12 -7
  14. data/doc/getting-started.md +3 -0
  15. data/doc/pan.md +19 -15
  16. data/doc/permissions.md +1 -0
  17. data/doc/platform-support.md +48 -0
  18. data/doc/releasing.md +12 -4
  19. data/doc/rpc.md +2 -2
  20. data/doc/sandboxing.md +5 -1
  21. data/doc/security.md +4 -3
  22. data/doc/shell.md +101 -60
  23. data/doc/tabs.md +3 -0
  24. data/doc/troubleshooting.md +12 -2
  25. data/doc/usage.md +7 -6
  26. data/kward.gemspec +5 -4
  27. data/lib/kward/agent.rb +7 -3
  28. data/lib/kward/ansi.rb +110 -10
  29. data/lib/kward/auth/anthropic_oauth.rb +7 -7
  30. data/lib/kward/cli/auth_commands.rb +34 -13
  31. data/lib/kward/cli/commands.rb +83 -62
  32. data/lib/kward/cli/doctor.rb +39 -17
  33. data/lib/kward/cli/hook_commands.rb +22 -12
  34. data/lib/kward/cli/interactive_turn.rb +144 -21
  35. data/lib/kward/cli/project_skills_commands.rb +8 -4
  36. data/lib/kward/cli/prompt_interface.rb +27 -0
  37. data/lib/kward/cli/rendering.rb +15 -9
  38. data/lib/kward/cli/runtime_helpers.rb +260 -55
  39. data/lib/kward/cli/slash_commands.rb +21 -12
  40. data/lib/kward/cli/tabs.rb +91 -13
  41. data/lib/kward/cli/tool_summaries.rb +14 -0
  42. data/lib/kward/cli.rb +55 -7
  43. data/lib/kward/cli_transcript_formatter.rb +11 -4
  44. data/lib/kward/compaction/token_estimator.rb +12 -6
  45. data/lib/kward/config_files.rb +94 -63
  46. data/lib/kward/detached_run.rb +44 -0
  47. data/lib/kward/editor_prompt.rb +46 -0
  48. data/lib/kward/editor_prompt_session.rb +28 -0
  49. data/lib/kward/interactive_pty_runner.rb +102 -28
  50. data/lib/kward/{ekwsh.rb → kwsh.rb} +103 -24
  51. data/lib/kward/kwshrc.rb +233 -0
  52. data/lib/kward/markdown_code_block.rb +136 -0
  53. data/lib/kward/model/client.rb +35 -39
  54. data/lib/kward/model/model_info.rb +3 -2
  55. data/lib/kward/model/payloads.rb +0 -2
  56. data/lib/kward/model/provider_catalog.rb +5 -0
  57. data/lib/kward/model/stream_parser.rb +20 -4
  58. data/lib/kward/pan/index.html.erb +3 -3
  59. data/lib/kward/pan/server.rb +23 -3
  60. data/lib/kward/persistent_shell_session.rb +843 -0
  61. data/lib/kward/project_files.rb +19 -6
  62. data/lib/kward/prompt_interface/composer_renderer.rb +45 -41
  63. data/lib/kward/prompt_interface/composer_state.rb +33 -24
  64. data/lib/kward/prompt_interface/editor/auto_indent.rb +27 -22
  65. data/lib/kward/prompt_interface/editor/controller.rb +99 -33
  66. data/lib/kward/prompt_interface/editor/endwise.rb +13 -4
  67. data/lib/kward/prompt_interface/editor/modes/modern.rb +4 -0
  68. data/lib/kward/prompt_interface/editor/modes/vibe.rb +298 -47
  69. data/lib/kward/prompt_interface/editor/renderer.rb +109 -6
  70. data/lib/kward/prompt_interface/editor/runner.rb +362 -0
  71. data/lib/kward/prompt_interface/editor/runner_state.rb +78 -0
  72. data/lib/kward/prompt_interface/editor/state.rb +21 -10
  73. data/lib/kward/prompt_interface/editor/syntax_highlighter.rb +68 -6
  74. data/lib/kward/prompt_interface/editor/vibe_state.rb +3 -3
  75. data/lib/kward/prompt_interface/editor/word_completion.rb +124 -0
  76. data/lib/kward/prompt_interface/file_overlay.rb +92 -22
  77. data/lib/kward/prompt_interface/key_handler.rb +75 -0
  78. data/lib/kward/prompt_interface/overlay_renderer.rb +7 -5
  79. data/lib/kward/prompt_interface/project_browser.rb +443 -17
  80. data/lib/kward/prompt_interface/runtime_state.rb +55 -2
  81. data/lib/kward/prompt_interface/screen.rb +2 -2
  82. data/lib/kward/prompt_interface/selection_prompt.rb +3 -1
  83. data/lib/kward/prompt_interface/slash_overlay.rb +19 -4
  84. data/lib/kward/prompt_interface/transcript_renderer.rb +12 -7
  85. data/lib/kward/prompt_interface.rb +169 -18
  86. data/lib/kward/prompts/commands.rb +2 -1
  87. data/lib/kward/prompts.rb +1 -1
  88. data/lib/kward/pty_output_sink.rb +47 -0
  89. data/lib/kward/rpc/auth_manager.rb +1 -1
  90. data/lib/kward/rpc/server.rb +2 -1
  91. data/lib/kward/rpc/transcript_normalizer.rb +7 -3
  92. data/lib/kward/scratchpad_languages.rb +74 -0
  93. data/lib/kward/scratchpad_runner.rb +155 -29
  94. data/lib/kward/shell_prompt.rb +52 -0
  95. data/lib/kward/shell_prompt_session.rb +58 -0
  96. data/lib/kward/terminal_keys.rb +13 -0
  97. data/lib/kward/terminal_text.rb +121 -0
  98. data/lib/kward/text_matcher.rb +18 -0
  99. data/lib/kward/tools/open_editor.rb +41 -0
  100. data/lib/kward/tools/prepare_shell_command.rb +28 -0
  101. data/lib/kward/tools/registry.rb +75 -8
  102. data/lib/kward/tools/replace_editor_buffer.rb +30 -0
  103. data/lib/kward/tools/run_shell_command.rb +24 -6
  104. data/lib/kward/tools/tool_call.rb +2 -1
  105. data/lib/kward/version.rb +1 -1
  106. data/templates/default/fulldoc/html/css/kward.css +0 -125
  107. data/templates/default/fulldoc/html/images/kward_workflow.svg +52 -0
  108. data/templates/default/fulldoc/html/setup.rb +1 -1
  109. data/templates/default/kward_navigation.rb +1 -0
  110. data/templates/default/layout/html/footer.erb +10 -0
  111. data/templates/default/layout/html/headers.erb +23 -0
  112. data/templates/default/layout/html/layout.erb +6 -18
  113. data/templates/default/layout/html/setup.rb +41 -2
  114. metadata +44 -8
  115. data/templates/default/fulldoc/html/images/kward_screen_1.png +0 -0
@@ -1,5 +1,6 @@
1
1
  require "fileutils"
2
- require_relative "../../scratchpad_runner"
2
+ require_relative "../../scratchpad_languages"
3
+ require_relative "runner"
3
4
 
4
5
  # Namespace for the Kward CLI agent runtime.
5
6
  module Kward
@@ -13,6 +14,10 @@ module Kward
13
14
  !@editor_state.nil?
14
15
  end
15
16
 
17
+ def editor_visible?
18
+ editor_active?
19
+ end
20
+
16
21
  def open_selected_file_in_editor(fallback_to_typed_path: false)
17
22
  path = selected_file_open_path
18
23
  if path
@@ -58,6 +63,7 @@ module Kward
58
63
  language: language
59
64
  )
60
65
  @editor_state.status = scratchpad_status_text(language)
66
+ @editor_agent_suspended = false
61
67
  @prompt_label = "Edit>"
62
68
  self.composer_input = ""
63
69
  self.composer_cursor = 0
@@ -95,6 +101,7 @@ module Kward
95
101
  end
96
102
 
97
103
  @editor_state = EditorState.new(path: full_path, content: File.exist?(full_path) ? File.read(full_path) : "", new_file: !File.exist?(full_path), editor_mode: current_editor_mode)
104
+ @editor_agent_suspended = false
98
105
  @prompt_label = "Edit>"
99
106
  self.composer_input = ""
100
107
  self.composer_cursor = 0
@@ -115,6 +122,7 @@ module Kward
115
122
  diff_view_mode = current_diff_view_mode
116
123
  viewer_content = diff_view_mode == DiffViewMode::SIDE_BY_SIDE ? side_by_side_diff_content(content.to_s) : content.to_s
117
124
  @editor_state = EditorState.new(path: path.to_s, content: viewer_content, new_file: true, editor_mode: current_editor_mode, readonly: true, diff_view: diff_view_mode)
125
+ @editor_agent_suspended = false
118
126
  @prompt_label = "Diff>"
119
127
  self.composer_input = ""
120
128
  self.composer_cursor = 0
@@ -236,13 +244,19 @@ module Kward
236
244
  end
237
245
 
238
246
  def close_editor
247
+ cancel_editor_runner
239
248
  disable_editor_mouse_reporting(force: true)
240
249
  restore_editor_cursor_shape_locked
250
+ @editor_runner_state = nil
251
+ @editor_runner_completion = nil
252
+ @editor_runner_output_visible = false
253
+ clear_editor_runner_selection
241
254
  @editor_text_width = nil
242
255
  @editor_save_as_active = false
243
256
  @editor_save_as_buffer = ""
244
257
  @editor_save_callback = nil
245
258
  @editor_state = nil
259
+ @editor_agent_suspended = false
246
260
  @prompt_label = "You>"
247
261
  self.composer_input = ""
248
262
  self.composer_cursor = 0
@@ -252,10 +266,17 @@ module Kward
252
266
 
253
267
  def handle_editor_key(key)
254
268
  return if key.nil?
269
+ return handle_editor_prompt_key(key) if @editor_prompt_active
270
+ return handle_editor_runner_output_key(key) if editor_runner_output_visible?
271
+
272
+ reset_editor_word_completion unless editor_word_completion_tab_key?(key) && !editor_search_active?
255
273
  mouse_result = handle_editor_mouse_key(key)
256
274
  return mouse_result unless mouse_result == false
257
275
  return handle_editor_save_as_key(key) if @editor_save_as_active
258
276
  return handle_readonly_editor_key(key) if @editor_state&.readonly?
277
+ return cancel_editor_runner if editor_runner_running? && @editor_runner_completion && key == TerminalKeys::CTRL_C
278
+ super_copy_result = handle_editor_super_copy_key(key)
279
+ return super_copy_result unless super_copy_result == false
259
280
  return handle_vibe_key(key) if @editor_state&.vibe?
260
281
  return handle_emacs_key(key) if @editor_state&.emacs?
261
282
  return handle_modern_key(key) if @editor_state&.modern?
@@ -312,6 +333,80 @@ module Kward
312
333
  end
313
334
  end
314
335
 
336
+ def handle_editor_super_copy_key(key)
337
+ sequence = parse_csi_u_key(key)
338
+ return false unless sequence
339
+ return false unless super_modifier?(sequence[:modifier]) && ctrl_code(sequence[:code]) == 99
340
+
341
+ queue_pending_keys(sequence[:remaining]) if sequence[:remaining] && !sequence[:remaining].empty?
342
+ return editor_search_cancel if editor_search_active?
343
+ return false unless editor_selection_active?
344
+
345
+ if @editor_state.vibe? && vibe_visual_mode?
346
+ vibe_yank_visual_selection
347
+ else
348
+ copy_editor_selection
349
+ end
350
+ end
351
+
352
+ def editor_prompt_action(instruction)
353
+ {
354
+ editor_prompt: {
355
+ instruction: instruction.to_s.strip,
356
+ display_input: ":prompt #{instruction.to_s.strip}"
357
+ }
358
+ }
359
+ end
360
+
361
+ def begin_editor_prompt
362
+ @editor_prompt_active = true
363
+ @editor_prompt_input = ""
364
+ @editor_state.status = "Prompt: "
365
+ true
366
+ end
367
+
368
+ def handle_editor_prompt_key(key)
369
+ sequence = parse_csi_u_key(key)
370
+ if sequence
371
+ queue_pending_keys(sequence[:remaining]) if sequence[:remaining] && !sequence[:remaining].empty?
372
+ return handle_editor_prompt_key("\r") if sequence[:code] == 13
373
+ return handle_editor_prompt_key("\e") if sequence[:code] == 27
374
+ return handle_editor_prompt_key("\b") if [8, 127].include?(sequence[:code])
375
+
376
+ text = csi_u_printable_text(sequence)
377
+ return handle_editor_prompt_key(text) if text
378
+
379
+ return true
380
+ end
381
+
382
+ case key
383
+ when "\e", TerminalKeys::CTRL_C, :escape
384
+ @editor_prompt_active = false
385
+ @editor_prompt_input = ""
386
+ @editor_state.status = EditorStatusText.default(readonly: @editor_state.readonly?, editor_mode: @editor_state.editor_mode)
387
+ true
388
+ when "\b", "\x7F"
389
+ @editor_prompt_input = @editor_prompt_input[0...-1]
390
+ @editor_state.status = "Prompt: #{@editor_prompt_input}"
391
+ when "\n", "\r"
392
+ instruction = @editor_prompt_input.strip
393
+ @editor_prompt_active = false
394
+ @editor_prompt_input = ""
395
+ if instruction.empty?
396
+ @editor_state.status = "Prompt instruction required"
397
+ true
398
+ else
399
+ editor_prompt_action(instruction)
400
+ end
401
+ else
402
+ if printable_key?(key)
403
+ @editor_prompt_input += key
404
+ @editor_state.status = "Prompt: #{@editor_prompt_input}"
405
+ end
406
+ true
407
+ end
408
+ end
409
+
315
410
  def handle_editor_csi_u_key(key)
316
411
  sequence = parse_csi_u_key(key)
317
412
  return false unless sequence
@@ -1318,45 +1413,16 @@ module Kward
1318
1413
  true
1319
1414
  end
1320
1415
 
1321
- def run_editor_buffer
1322
- return false unless @editor_state
1323
-
1324
- language = @editor_state.language || editor_syntax_language
1325
- result = ScratchpadRunner.run(language, @editor_state.buffer)
1326
- @editor_state.replace_range(0, @editor_state.buffer.length, result.buffer)
1327
- @editor_state.status = "Ran #{language} (exit #{result.exit_status})"
1328
- true
1329
- rescue StandardError => e
1330
- @editor_state.status = "Run failed: #{e.message}" if @editor_state
1331
- false
1332
- end
1333
-
1334
1416
  def normalize_scratchpad_language(language)
1335
- case language.to_s.strip.downcase
1336
- when "", "text", "txt"
1337
- :text
1338
- when "markdown", "md"
1339
- :markdown
1340
- when "ruby", "rb"
1341
- :ruby
1342
- else
1343
- :text
1344
- end
1417
+ ScratchpadLanguages.normalize(language)
1345
1418
  end
1346
1419
 
1347
1420
  def scratchpad_display_path(language)
1348
- case language.to_sym
1349
- when :markdown
1350
- "scratchpad.md"
1351
- when :ruby
1352
- "scratchpad.rb"
1353
- else
1354
- "scratchpad.txt"
1355
- end
1421
+ ScratchpadLanguages.display_path(language)
1356
1422
  end
1357
1423
 
1358
1424
  def scratchpad_status_text(language)
1359
- runnable = language.to_sym == :ruby
1425
+ runnable = ScratchpadLanguages.runnable?(language)
1360
1426
  case current_editor_mode
1361
1427
  when "vibe"
1362
1428
  runnable ? "NORMAL · i insert · :w filename save · :q quit · :run run" : "NORMAL · i insert · :w filename save · :q quit"
@@ -136,7 +136,7 @@ module Kward
136
136
  line_index, column = @editor_state.cursor_line_and_column
137
137
  return nil unless editor_endwise_line_exists?(line_index)
138
138
 
139
- language = editor_syntax_language
139
+ language = editor_effective_syntax_language(line_index)
140
140
  definition = ENDWISE_LANGUAGES[language]
141
141
  return nil unless definition
142
142
 
@@ -190,7 +190,8 @@ module Kward
190
190
  line_index >= 0 && line_index < @editor_state.lines.length
191
191
  end
192
192
 
193
- def editor_endwise_line_opens_block?(line_index, language = editor_syntax_language)
193
+ def editor_endwise_line_opens_block?(line_index, language = nil)
194
+ language ||= editor_effective_syntax_language(line_index)
194
195
  code = editor_endwise_code_line_at(line_index, language)
195
196
  return false if editor_endwise_ignored_definition?(code, language)
196
197
 
@@ -200,7 +201,7 @@ module Kward
200
201
  end
201
202
 
202
203
  def editor_endwise_closing_keyword_for_line(line_index, column, called_with_modifier: false)
203
- language = editor_syntax_language
204
+ language = editor_effective_syntax_language(line_index)
204
205
  definition = ENDWISE_LANGUAGES[language]
205
206
  return nil unless definition
206
207
 
@@ -221,6 +222,9 @@ module Kward
221
222
  return close if @editor_state.lines.length <= scan_line + 1
222
223
 
223
224
  line_below = @editor_state.lines[scan_line + 1].to_s
225
+ next_language = editor_effective_syntax_language(scan_line + 1)
226
+ return close if next_language != language
227
+
224
228
  code_below = editor_endwise_code_line_at(scan_line + 1, language)
225
229
  closes_any_block = editor_endwise_closes_block?(code_below, language)
226
230
  closes_this_block = editor_endwise_closes_with?(code_below, close)
@@ -254,6 +258,8 @@ module Kward
254
258
  end
255
259
 
256
260
  def editor_endwise_code_line_at(line_index, language)
261
+ return "" unless editor_effective_syntax_language(line_index) == language
262
+
257
263
  definition = ENDWISE_LANGUAGES[language]
258
264
  line = @editor_state.lines[line_index].to_s
259
265
  return "" if editor_endwise_inside_block_comment?(line_index, definition.fetch(:block_comments))
@@ -263,7 +269,10 @@ module Kward
263
269
 
264
270
  def editor_endwise_inside_block_comment?(line_index, block_comments)
265
271
  active_comment = nil
266
- @editor_state.lines.first(line_index + 1).each_with_index do |line, index|
272
+ first_index = editor_syntax_context_start(line_index)
273
+ first_index = 0 unless first_index
274
+ (first_index..line_index).each do |index|
275
+ line = @editor_state.lines[index].to_s
267
276
  if active_comment
268
277
  target_line = index == line_index
269
278
  active_comment = nil if line.match?(active_comment.fetch(:end))
@@ -198,6 +198,8 @@ module Kward
198
198
 
199
199
  def handle_modern_key_binding(key)
200
200
  case key
201
+ when TerminalKeys::CTRL_PERIOD
202
+ begin_editor_prompt
201
203
  when TerminalKeys::CTRL_SPACE
202
204
  true
203
205
  when TerminalKeys::CTRL_C
@@ -221,6 +223,8 @@ module Kward
221
223
  end
222
224
 
223
225
  case normalized_code
226
+ when 46
227
+ begin_editor_prompt
224
228
  when 13
225
229
  return false if editor_search_active?
226
230