kward 0.71.0 → 0.72.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 (116) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +41 -1
  3. data/Gemfile.lock +2 -2
  4. data/README.md +4 -0
  5. data/doc/agent-tools.md +15 -6
  6. data/doc/authentication.md +22 -1
  7. data/doc/code-search.md +42 -2
  8. data/doc/configuration.md +106 -3
  9. data/doc/context-budgeting.md +136 -0
  10. data/doc/context-tools.md +16 -3
  11. data/doc/editor.md +394 -0
  12. data/doc/extensibility.md +16 -7
  13. data/doc/files.md +100 -0
  14. data/doc/getting-started.md +25 -18
  15. data/doc/git.md +122 -0
  16. data/doc/memory.md +24 -4
  17. data/doc/personas.md +34 -5
  18. data/doc/plugins.md +72 -1
  19. data/doc/releasing.md +37 -9
  20. data/doc/rpc.md +74 -4
  21. data/doc/session-management.md +35 -1
  22. data/doc/shell.md +286 -0
  23. data/doc/tabs.md +122 -0
  24. data/doc/troubleshooting.md +77 -1
  25. data/doc/usage.md +53 -7
  26. data/doc/web-search.md +12 -4
  27. data/doc/workspace-tools.md +51 -12
  28. data/examples/plugins/space_invaders.rb +377 -0
  29. data/lib/kward/agent.rb +1 -1
  30. data/lib/kward/cli/commands.rb +33 -2
  31. data/lib/kward/cli/git.rb +150 -0
  32. data/lib/kward/cli/interactive_turn.rb +73 -9
  33. data/lib/kward/cli/plugins.rb +54 -4
  34. data/lib/kward/cli/prompt_interface.rb +32 -1
  35. data/lib/kward/cli/runtime_helpers.rb +133 -3
  36. data/lib/kward/cli/sessions.rb +2 -2
  37. data/lib/kward/cli/settings.rb +218 -9
  38. data/lib/kward/cli/slash_commands.rb +415 -2
  39. data/lib/kward/cli/tabs.rb +695 -0
  40. data/lib/kward/cli.rb +158 -26
  41. data/lib/kward/config_files.rb +123 -1
  42. data/lib/kward/context_budget_meter.rb +44 -0
  43. data/lib/kward/conversation.rb +12 -4
  44. data/lib/kward/editor_mode.rb +25 -0
  45. data/lib/kward/ekwsh.rb +362 -0
  46. data/lib/kward/plugin_registry.rb +61 -0
  47. data/lib/kward/project_files.rb +52 -0
  48. data/lib/kward/prompt_history.rb +82 -0
  49. data/lib/kward/prompt_interface/composer_controller.rb +69 -1
  50. data/lib/kward/prompt_interface/composer_renderer.rb +109 -13
  51. data/lib/kward/prompt_interface/composer_state.rb +96 -27
  52. data/lib/kward/prompt_interface/editor/auto_close_pairs.rb +123 -0
  53. data/lib/kward/prompt_interface/editor/auto_indent.rb +509 -0
  54. data/lib/kward/prompt_interface/editor/buffer.rb +109 -0
  55. data/lib/kward/prompt_interface/editor/controller.rb +1018 -0
  56. data/lib/kward/prompt_interface/editor/endwise.rb +321 -0
  57. data/lib/kward/prompt_interface/editor/file_marker.rb +40 -0
  58. data/lib/kward/prompt_interface/editor/indent_navigation.rb +61 -0
  59. data/lib/kward/prompt_interface/editor/kill_ring.rb +78 -0
  60. data/lib/kward/prompt_interface/editor/modes/emacs.rb +259 -0
  61. data/lib/kward/prompt_interface/editor/modes/modern.rb +353 -0
  62. data/lib/kward/prompt_interface/editor/modes/vibe.rb +1962 -0
  63. data/lib/kward/prompt_interface/editor/renderer.rb +243 -0
  64. data/lib/kward/prompt_interface/editor/search.rb +76 -0
  65. data/lib/kward/prompt_interface/editor/selections.rb +120 -0
  66. data/lib/kward/prompt_interface/editor/state.rb +1249 -0
  67. data/lib/kward/prompt_interface/editor/status_text.rb +23 -0
  68. data/lib/kward/prompt_interface/editor/syntax_highlighter.rb +420 -0
  69. data/lib/kward/prompt_interface/editor/undo_history.rb +46 -0
  70. data/lib/kward/prompt_interface/editor/vibe_state.rb +44 -0
  71. data/lib/kward/prompt_interface/file_overlay.rb +211 -0
  72. data/lib/kward/prompt_interface/git_prompt.rb +299 -0
  73. data/lib/kward/prompt_interface/interactive/controller.rb +186 -0
  74. data/lib/kward/prompt_interface/interactive/renderer.rb +71 -0
  75. data/lib/kward/prompt_interface/interactive/state.rb +62 -0
  76. data/lib/kward/prompt_interface/key_handler.rb +387 -35
  77. data/lib/kward/prompt_interface/overlay_renderer.rb +21 -2
  78. data/lib/kward/prompt_interface/project_browser.rb +524 -0
  79. data/lib/kward/prompt_interface/question_prompt.rb +98 -50
  80. data/lib/kward/prompt_interface/runtime_state.rb +43 -0
  81. data/lib/kward/prompt_interface/screen.rb +16 -0
  82. data/lib/kward/prompt_interface/selection_prompt.rb +7 -13
  83. data/lib/kward/prompt_interface/stream_state.rb +7 -0
  84. data/lib/kward/prompt_interface/transcript_buffer.rb +6 -0
  85. data/lib/kward/prompt_interface.rb +286 -8
  86. data/lib/kward/prompts/commands.rb +5 -0
  87. data/lib/kward/prompts.rb +2 -0
  88. data/lib/kward/rpc/server.rb +42 -3
  89. data/lib/kward/rpc/session_manager.rb +35 -47
  90. data/lib/kward/rpc/session_tree_rows.rb +9 -115
  91. data/lib/kward/rpc/tool_event_normalizer.rb +1 -1
  92. data/lib/kward/session_store.rb +44 -0
  93. data/lib/kward/session_tree_nodes.rb +136 -0
  94. data/lib/kward/session_tree_renderer.rb +9 -131
  95. data/lib/kward/tab_store.rb +47 -0
  96. data/lib/kward/text_boundary.rb +25 -0
  97. data/lib/kward/tools/context_budget_stats.rb +54 -0
  98. data/lib/kward/tools/context_for_task.rb +202 -0
  99. data/lib/kward/tools/read_file.rb +8 -4
  100. data/lib/kward/tools/registry.rb +62 -16
  101. data/lib/kward/tools/tool_call.rb +10 -0
  102. data/lib/kward/version.rb +1 -1
  103. data/lib/kward/workers/git_guard.rb +68 -0
  104. data/lib/kward/workers/live_view.rb +49 -0
  105. data/lib/kward/workers/manager.rb +288 -0
  106. data/lib/kward/workers/store.rb +72 -0
  107. data/lib/kward/workers/tool_policy.rb +23 -0
  108. data/lib/kward/workers/worker.rb +82 -0
  109. data/lib/kward/workers/write_lock.rb +38 -0
  110. data/lib/kward/workers.rb +7 -0
  111. data/lib/kward/workspace.rb +110 -24
  112. data/templates/default/fulldoc/html/css/kward.css +107 -36
  113. data/templates/default/kward_navigation.rb +12 -1
  114. data/templates/default/layout/html/layout.erb +4 -2
  115. data/templates/default/layout/html/setup.rb +6 -0
  116. metadata +53 -1
@@ -1,21 +1,44 @@
1
+ require "base64"
2
+ require "find"
1
3
  require "io/console"
4
+ require "pathname"
5
+ require "rbconfig"
2
6
  require "thread"
7
+ require_relative "project_files"
8
+ require_relative "prompt_history"
3
9
  require "tty-cursor"
4
10
  require "tty-reader"
5
11
  require "tty-screen"
6
12
  require_relative "ansi"
13
+ require_relative "editor_mode"
7
14
  require_relative "prompt_interface/banner"
8
15
  require_relative "prompt_interface/composer_state"
16
+ require_relative "prompt_interface/editor/state"
9
17
  require_relative "prompt_interface/transcript_buffer"
10
18
  require_relative "prompt_interface/transcript_renderer"
11
19
  require_relative "prompt_interface/prompt_renderer"
12
20
  require_relative "prompt_interface/stream_state"
13
21
  require_relative "prompt_interface/slash_overlay"
22
+ require_relative "prompt_interface/file_overlay"
23
+ require_relative "prompt_interface/project_browser"
14
24
  require_relative "prompt_interface/selection_prompt"
15
25
  require_relative "prompt_interface/question_prompt"
26
+ require_relative "prompt_interface/git_prompt"
16
27
  require_relative "prompt_interface/overlay_renderer"
28
+ require_relative "prompt_interface/editor/renderer"
29
+ require_relative "prompt_interface/editor/syntax_highlighter"
30
+ require_relative "prompt_interface/editor/auto_close_pairs"
31
+ require_relative "prompt_interface/editor/endwise"
32
+ require_relative "prompt_interface/editor/auto_indent"
17
33
  require_relative "prompt_interface/composer_renderer"
18
34
  require_relative "prompt_interface/composer_controller"
35
+ require_relative "prompt_interface/editor/modes/modern"
36
+ require_relative "prompt_interface/editor/modes/emacs"
37
+ require_relative "prompt_interface/editor/modes/vibe"
38
+ require_relative "prompt_interface/editor/controller"
39
+ require_relative "prompt_interface/interactive/controller"
40
+ require_relative "prompt_interface/interactive/renderer"
41
+ require_relative "prompt_interface/interactive/state"
19
42
  require_relative "prompt_interface/layout"
20
43
  require_relative "prompt_interface/screen"
21
44
  require_relative "prompt_interface/key_handler"
@@ -39,23 +62,38 @@ module Kward
39
62
  SPINNER_FRAMES = %w[⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏].freeze
40
63
  SPINNER_INTERVAL = 0.1
41
64
  FOOTER_REFRESH_INTERVAL = 1.0
65
+ COMPOSER_STATUS_REFRESH_INTERVAL = 1.0
42
66
  COMPOSER_MAX_INPUT_ROWS = 6
43
67
  TRANSCRIPT_BUFFER_LIMIT = 200_000
44
68
  BANNER_MESSAGE = Banner::MESSAGE
45
69
 
46
70
  include SlashOverlay
71
+ include FileOverlay
72
+ include ProjectBrowser
47
73
  include SelectionPrompt
48
74
  include QuestionPrompt
75
+ include GitPrompt
49
76
  include OverlayRenderer
77
+ include EditorRenderer
78
+ include EditorSyntaxHighlighter
79
+ include EditorAutoClosePairs
80
+ include EditorEndwise
81
+ include EditorAutoIndent
50
82
  include ComposerRenderer
51
83
  include ComposerController
84
+ include ModernEditorMode
85
+ include EmacsEditorMode
86
+ include VibeEditorMode
87
+ include EditorController
88
+ include InteractiveRenderer
89
+ include InteractiveState
52
90
  include Layout
53
91
  include Screen
54
92
  include KeyHandler
55
93
  include RuntimeState
56
94
  include TranscriptRenderer
57
95
  include PromptRenderer
58
- KEYBOARD_PROTOCOL_ENABLE = "\e[>1u".freeze
96
+ KEYBOARD_PROTOCOL_ENABLE = "\e[>25u".freeze
59
97
  KEYBOARD_PROTOCOL_RESTORE = "\e[<u".freeze
60
98
  BRACKETED_PASTE_ENABLE = "\e[?2004h".freeze
61
99
  BRACKETED_PASTE_RESTORE = "\e[?2004l".freeze
@@ -65,6 +103,8 @@ module Kward
65
103
  SYNCHRONIZED_OUTPUT_DISABLE = "\e[?2026l".freeze
66
104
  CURSOR_SHOW = "\e[?25h".freeze
67
105
  CURSOR_HIDE = "\e[?25l".freeze
106
+ CURSOR_SHAPE_DEFAULT = "\e[0 q".freeze
107
+ CURSOR_SHAPE_BAR = "\e[6 q".freeze
68
108
  SHIFT_ENTER_SEQUENCES = ["\e[13;2u", "\e[13;2~", "\e[27;2;13~", "\e\r", "\e\n"].freeze
69
109
  EXIT_INPUT = :exit_input
70
110
  CANCEL_INPUT = :cancel_input
@@ -82,12 +122,14 @@ module Kward
82
122
  end
83
123
  end
84
124
 
85
- 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)
125
+ 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)
86
126
  @input_io = input
87
127
  @output_io = output
88
128
  @reader = TTY::Reader.new(input: input, output: output, interrupt: :error)
89
129
  @mutex = Mutex.new
130
+ @prompt_history = prompt_history
90
131
  @composer = ComposerState.new
132
+ load_history(@prompt_history.values) if @prompt_history
91
133
  self.composer_input = @composer.input
92
134
  self.composer_cursor = @composer.cursor
93
135
  @started = false
@@ -99,6 +141,8 @@ module Kward
99
141
  @spinner_frame_index = 0
100
142
  @last_spinner_tick = monotonic_now
101
143
  @last_footer_refresh = monotonic_now
144
+ @last_composer_status_refresh = 0.0
145
+ @cached_composer_status_text = nil
102
146
  @prompt_label = "You>"
103
147
  @assistant_label = "Assistant"
104
148
  @stream_state = StreamState.new
@@ -109,18 +153,32 @@ module Kward
109
153
  @transcript_viewport_rows = 0
110
154
  @restoring_transcript = false
111
155
  @pending_keys = []
156
+ @completion_provider = nil
112
157
  @original_console_mode = nil
113
158
  @raw_mode_active = false
114
159
  @slash_commands = normalize_slash_commands(slash_commands)
115
160
  @slash_selection_index = 0
116
161
  @slash_overlay_dismissed_input = nil
162
+ @file_selection_index = 0
163
+ @file_overlay_dismissed_token = nil
164
+ @file_open_dismissed_token = nil
165
+ @file_editor_open_status = nil
166
+ @file_mention_paths = nil
167
+ @project_browser_state = nil
168
+ @project_browser_restore_after_editor = false
169
+ @editor_state = nil
170
+ @interactive_state = nil
171
+ @last_interactive_tick = monotonic_now
117
172
  @select_state = nil
118
173
  @question_state = nil
174
+ @question_prompt_active = false
175
+ @git_state = nil
119
176
  @last_width = screen_width
120
177
  @last_height = screen_height
121
178
  @reserved_rows = 0
122
179
  @color_enabled = ANSI.enabled?(output)
123
180
  @cursor_visible = true
181
+ @editor_bar_cursor_active = false
124
182
  @synchronized_output_depth = 0
125
183
  @overlay_settings = normalize_overlay_settings(overlay_settings)
126
184
  @footer = footer
@@ -129,6 +187,21 @@ module Kward
129
187
  @attachment_badges = attachment_badges
130
188
  @attachment_parser = attachment_parser
131
189
  @banner = Banner.new(message: banner_message, screen_height: method(:screen_height))
190
+ @tabs = []
191
+ @active_tab_index = 0
192
+ @tab_keybindings = normalize_tab_keybindings(tab_keybindings)
193
+ @editor_mode = normalize_editor_mode(editor_mode)
194
+ @editor_mode_source = editor_mode_source
195
+ @editor_auto_indent = editor_auto_indent != false
196
+ @editor_auto_indent_source = editor_auto_indent_source
197
+ @editor_auto_close_pairs = editor_auto_close_pairs != false
198
+ @editor_auto_close_pairs_source = editor_auto_close_pairs_source
199
+ @editor_soft_wrap = editor_soft_wrap != false
200
+ @editor_soft_wrap_source = editor_soft_wrap_source
201
+ @editor_bar_cursor = editor_bar_cursor != false
202
+ @editor_bar_cursor_source = editor_bar_cursor_source
203
+ @editor_line_numbers = normalize_editor_line_numbers(editor_line_numbers)
204
+ @editor_line_numbers_source = editor_line_numbers_source
132
205
  end
133
206
 
134
207
  def start(render: true)
@@ -138,6 +211,7 @@ module Kward
138
211
  enter_raw_mode_locked
139
212
  @started = true
140
213
  @asking = true
214
+ disable_editor_mouse_reporting(force: true) unless editor_active?
141
215
  @output_io.print(KEYBOARD_PROTOCOL_ENABLE)
142
216
  @output_io.print(BRACKETED_PASTE_ENABLE)
143
217
  render_prompt_locked if render
@@ -150,8 +224,10 @@ module Kward
150
224
 
151
225
  clear_prompt_for_output_locked
152
226
  restore_scroll_region_locked
227
+ disable_editor_mouse_reporting(force: true)
153
228
  @output_io.print(BRACKETED_PASTE_RESTORE)
154
229
  @output_io.print(KEYBOARD_PROTOCOL_RESTORE)
230
+ restore_editor_cursor_shape_locked
155
231
  set_cursor_visible_locked(true, force: true)
156
232
  @output_io.puts
157
233
  @output_io.flush
@@ -220,6 +296,54 @@ module Kward
220
296
  end
221
297
  end
222
298
 
299
+ def with_completion_provider(provider)
300
+ previous = @completion_provider
301
+ @completion_provider = provider
302
+ yield
303
+ ensure
304
+ @completion_provider = previous
305
+ end
306
+
307
+ def editing_file?
308
+ @mutex.synchronize { editor_active? }
309
+ end
310
+
311
+ def edit_file(path, base_dir: Dir.pwd, allow_new: true)
312
+ start(render: false)
313
+ opened = @mutex.synchronize do
314
+ open_editor(path, allow_new: allow_new, base_dir: base_dir, restrict_to_workspace: false).tap do
315
+ render_prompt_locked
316
+ end
317
+ end
318
+ return false unless opened
319
+
320
+ run_editor
321
+ end
322
+
323
+ def run_editor
324
+ loop do
325
+ key = read_key(nonblock: true)
326
+ action = nil
327
+ editor_open = @mutex.synchronize do
328
+ if key.nil?
329
+ resized = handle_resize_locked
330
+ footer_refreshed = tick_footer_locked
331
+ render_prompt_locked if resized || footer_refreshed
332
+ else
333
+ result = handle_key(key)
334
+ action = result if prompt_action_result?(result)
335
+ render_prompt_locked unless result.is_a?(String) || result == EXIT_INPUT || prompt_action_result?(result)
336
+ end
337
+ editor_active?
338
+ end
339
+ return action if action
340
+ break unless editor_open
341
+
342
+ sleep 0.02 if key.nil?
343
+ end
344
+ true
345
+ end
346
+
223
347
  def ask(message = "You>")
224
348
  was_composing = @started && @asking
225
349
  start
@@ -250,10 +374,10 @@ module Kward
250
374
  render_prompt_locked if resized || footer_refreshed
251
375
  else
252
376
  result = handle_key(key)
253
- render_prompt_locked unless result.is_a?(String) || result == EXIT_INPUT
377
+ render_prompt_locked unless result.is_a?(String) || result == EXIT_INPUT || prompt_action_result?(result)
254
378
  end
255
379
  end
256
- return result if result.is_a?(String)
380
+ return result if result.is_a?(String) || prompt_action_result?(result)
257
381
  return nil if result == EXIT_INPUT
258
382
 
259
383
  sleep 0.02 if key.nil?
@@ -323,7 +447,10 @@ module Kward
323
447
  start
324
448
  saved_state = nil
325
449
  answers = []
326
- @mutex.synchronize { saved_state = begin_question_prompt_state }
450
+ @mutex.synchronize do
451
+ @question_prompt_active = true
452
+ saved_state = begin_question_prompt_state
453
+ end
327
454
 
328
455
  questions.each_with_index do |question, index|
329
456
  answer = ask_single_user_question(question, index + 1, questions.length)
@@ -342,7 +469,127 @@ module Kward
342
469
  end
343
470
 
344
471
  def modal_active?
345
- @mutex.synchronize { !@question_state.nil? || !@select_state.nil? }
472
+ @mutex.synchronize { modal_active_locked? }
473
+ end
474
+
475
+ def interactive_active?
476
+ @mutex.synchronize { interactive_active_locked? }
477
+ end
478
+
479
+ def interactive_exited?
480
+ @mutex.synchronize do
481
+ return false unless @interactive_state
482
+
483
+ @interactive_state[:controller].exited?
484
+ end
485
+ end
486
+
487
+ def finish_interactive
488
+ @mutex.synchronize do
489
+ return unless @interactive_state
490
+
491
+ snapshot = @interactive_state[:snapshot]
492
+ @interactive_state = nil
493
+ restore_composer_snapshot_locked(snapshot)
494
+ redraw_screen_locked if @started
495
+ @output_io.flush
496
+ end
497
+ end
498
+
499
+ def start_interactive(title:, rows:, fps:)
500
+ snapshot = composer_snapshot
501
+ controller = InteractiveController.new(width: interactive_canvas_width, height: rows, fps: fps)
502
+ start
503
+ @mutex.synchronize do
504
+ @interactive_state = {
505
+ title: title.to_s,
506
+ rows: rows,
507
+ controller: controller,
508
+ snapshot: snapshot
509
+ }
510
+ @last_interactive_tick = monotonic_now
511
+ @asking = true
512
+ @busy = false
513
+ @last_composer_rows = []
514
+ render_prompt_locked
515
+ end
516
+ controller
517
+ end
518
+
519
+ def update_tabs(labels:, active_index: 0)
520
+ @mutex.synchronize do
521
+ @tabs = Array(labels).map { |label| normalize_tab_label(label) }
522
+ @active_tab_index = active_index.to_i
523
+ render_prompt_locked if @started && @asking
524
+ end
525
+ end
526
+
527
+ def composer_snapshot
528
+ @mutex.synchronize do
529
+ {
530
+ composer: @composer,
531
+ prompt_label: @prompt_label
532
+ }
533
+ end
534
+ end
535
+
536
+ def tab_view_snapshot
537
+ @mutex.synchronize do
538
+ {
539
+ composer: @composer.dup,
540
+ prompt_label: @prompt_label.dup,
541
+ editor_state: @editor_state&.dup,
542
+ transcript_buffer: @transcript_buffer.dup,
543
+ transcript_viewport_rows: @transcript_viewport_rows,
544
+ stream_state: @stream_state.dup
545
+ }
546
+ end
547
+ end
548
+
549
+ def restore_composer_snapshot(snapshot)
550
+ @mutex.synchronize do
551
+ restore_composer_snapshot_locked(snapshot)
552
+ restore_editor_snapshot_locked(snapshot)
553
+ redraw_screen_locked if @started
554
+ end
555
+ end
556
+
557
+ def restore_tab_view_snapshot(snapshot)
558
+ @mutex.synchronize do
559
+ restore_composer_snapshot_locked(snapshot)
560
+ restore_editor_snapshot_locked(snapshot)
561
+ @transcript_buffer = snapshot[:transcript_buffer] || TranscriptBuffer.new(limit: TRANSCRIPT_BUFFER_LIMIT)
562
+ @transcript_viewport_rows = snapshot[:transcript_viewport_rows].to_i
563
+ @stream_state = snapshot[:stream_state] || StreamState.new
564
+ @last_composer_rows = []
565
+ redraw_screen_locked if @started
566
+ end
567
+ end
568
+
569
+ def restore_composer_snapshot_locked(snapshot)
570
+ @composer = snapshot[:composer] || new_composer_state_with_history
571
+ @prompt_label = snapshot[:prompt_label].to_s.empty? ? "You>" : snapshot[:prompt_label].to_s
572
+ self.composer_input = @composer.input
573
+ self.composer_cursor = @composer.cursor
574
+ @last_composer_rows = []
575
+ end
576
+
577
+ def new_composer_state_with_history
578
+ composer = ComposerState.new
579
+ composer.load_history(@prompt_history.values) if @prompt_history
580
+ composer
581
+ end
582
+
583
+ def restore_editor_snapshot_locked(snapshot)
584
+ editor_was_active = editor_active?
585
+ @editor_state = snapshot[:editor_state]&.dup
586
+ editor_is_active = editor_active?
587
+
588
+ if editor_is_active
589
+ enable_editor_mouse_reporting unless editor_was_active
590
+ else
591
+ disable_editor_mouse_reporting(force: true)
592
+ end
346
593
  end
347
594
 
348
595
  def update_overlay_settings(settings)
@@ -409,6 +656,22 @@ module Kward
409
656
  def poll_input
410
657
  key = read_key(nonblock: true)
411
658
  @mutex.synchronize do
659
+ if interactive_active_locked?
660
+ if key.nil?
661
+ resized = handle_resize_locked
662
+ ticked = tick_interactive_locked
663
+ render_prompt_locked if resized || ticked
664
+ return :interactive_exited if @interactive_state[:controller].exited?
665
+ return nil
666
+ end
667
+
668
+ route_interactive_key(key)
669
+ ticked = tick_interactive_locked
670
+ render_prompt_locked if ticked
671
+ return :interactive_exited if @interactive_state[:controller].exited?
672
+ return nil
673
+ end
674
+
412
675
  if key.nil?
413
676
  resized = handle_resize_locked
414
677
  spun = tick_spinner_locked
@@ -417,8 +680,13 @@ module Kward
417
680
  return nil
418
681
  end
419
682
 
683
+ if modal_active_locked?
684
+ queue_pending_keys(key)
685
+ return nil
686
+ end
687
+
420
688
  result = handle_key(key)
421
- render_prompt_locked unless [EXIT_INPUT, CANCEL_INPUT].include?(result)
689
+ render_prompt_locked unless [EXIT_INPUT, CANCEL_INPUT].include?(result) || prompt_action_result?(result)
422
690
  [EXIT_INPUT, CANCEL_INPUT].include?(result) ? result : result
423
691
  end
424
692
  end
@@ -479,6 +747,14 @@ module Kward
479
747
  end
480
748
  end
481
749
 
750
+ def refresh_composer_status
751
+ @mutex.synchronize do
752
+ @cached_composer_status_text = nil
753
+ @last_composer_status_refresh = 0.0
754
+ render_prompt_locked if @started && @asking
755
+ end
756
+ end
757
+
482
758
  def clear_transcript
483
759
  @mutex.synchronize do
484
760
  @transcript_buffer.clear
@@ -493,7 +769,9 @@ module Kward
493
769
 
494
770
  private
495
771
 
496
-
772
+ def modal_active_locked?
773
+ @question_prompt_active || !@question_state.nil? || !@select_state.nil? || !@git_state.nil?
774
+ end
497
775
 
498
776
 
499
777
 
@@ -25,6 +25,11 @@ module Kward
25
25
  { name: "model", description: "Select the default model.", argument_hint: "" },
26
26
  { name: "reasoning", description: "Select reasoning effort.", argument_hint: "" },
27
27
  { name: "reload", description: "Reload installed plugins.", argument_hint: "" },
28
+ { name: "workers", description: "Open the worker pipeline.", argument_hint: "[new|do <task>]" },
29
+ { name: "git", description: "Review uncommitted changes and commit them.", argument_hint: "" },
30
+ { name: "files", description: "Browse project files.", argument_hint: "" },
31
+ { name: "shell", description: "Open the embedded Kward shell.", argument_hint: "" },
32
+ { name: "tab", description: "Manage tabs.", argument_hint: "[1-n|move|close|new|name]" },
28
33
  { name: "status", description: "Show the current status message.", argument_hint: "" },
29
34
  { name: "stats", description: "Show telemetry logging stats.", argument_hint: "[range]" },
30
35
  { name: "memory", description: "Inspect and manage Kward memory.", argument_hint: "[enable|disable|auto-summary|core|add|list|forget|promote|relax|inspect|why|summarize]" }
data/lib/kward/prompts.rb CHANGED
@@ -35,6 +35,8 @@ module Kward
35
35
  You are Kward, a concise practical CLI coding agent. Use tools to understand and modify software projects. Inspect files before changing them, make the smallest correct change, preserve existing style, and summarize what changed. Be honest about limitations.
36
36
 
37
37
  For web research, use web_search to discover sources, fetch_content for important human-readable pages, and fetch_raw for machine-readable resources such as JSON, YAML, XML, RSS, OpenAPI specs, and plain text. Prefer official or primary sources and cite or mention the URLs you relied on.
38
+
39
+ Manage code context deliberately. Prefer context_for_task, summarize_file_structure, and read_file mode="outline"/"preview" before broad reads. Escalate to read_file mode="range" for exact lines, and use mode="full" only when focused context is insufficient. Use context_budget_stats when asked about context savings.
38
40
  PROMPT
39
41
  end
40
42
 
@@ -5,6 +5,7 @@ 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"
8
9
  require_relative "../workspace"
9
10
  require_relative "../telemetry/logger"
10
11
  require_relative "../telemetry/stats"
@@ -60,14 +61,18 @@ module Kward
60
61
  "memory/forget", "memory/promote", "memory/relax", "memory/inspect",
61
62
  "memory/why", "memory/summarize"
62
63
  ].freeze
64
+ WORKER_METHODS = ["workers/list", "workers/show"].freeze
63
65
 
64
66
  # Creates the RPC server and its stateful managers.
65
- def initialize(input: $stdin, output: $stdout, error_output: $stderr, client: Client.new)
67
+ def initialize(input: $stdin, output: $stdout, error_output: $stderr, client: Client.new, experimental_workers: false)
66
68
  @transport = Transport.new(input: input, output: output)
67
69
  @error_output = error_output
70
+ @client = client
68
71
  @config_manager = ConfigManager.new
69
72
  @session_manager = SessionManager.new(server: self, client: client, config_manager: @config_manager)
70
73
  @auth_manager = AuthManager.new(server: self, config_manager: @config_manager)
74
+ @worker_store = Workers::Store.new
75
+ @experimental_workers = experimental_workers
71
76
  @shutdown = false
72
77
  end
73
78
 
@@ -88,7 +93,7 @@ module Kward
88
93
  end
89
94
  end
90
95
  ensure
91
- @session_manager.cleanup_unused_sessions
96
+ @session_manager.shutdown_sessions
92
97
  end
93
98
 
94
99
  # Sends a redacted JSON-RPC notification to the client.
@@ -222,6 +227,12 @@ module Kward
222
227
  @session_manager.memory_why(session_id: params["sessionId"])
223
228
  when "memory/summarize"
224
229
  @session_manager.memory_summarize(session_id: params.fetch("sessionId"))
230
+ when "workers/list"
231
+ require_experimental_workers!
232
+ workers_list(params)
233
+ when "workers/show"
234
+ require_experimental_workers!
235
+ workers_show(params)
225
236
  when "auth/status"
226
237
  @auth_manager.status
227
238
  when "auth/providers"
@@ -343,7 +354,7 @@ module Kward
343
354
  reasoning: { start: false, delta: true, end: false },
344
355
  modelRetry: { supported: true, event: "modelRetry" },
345
356
  steering: { supported: @session_manager.in_flight_steer_supported?, event: "turnSteered", mode: @session_manager.in_flight_steer_supported? ? "native" : "unsupported" },
346
- tools: { call: true, update: false, result: true, normalizedMetadata: true, diffs: true, changedFiles: false, workspaceGuardrails: workspace_guardrails_enabled? },
357
+ tools: { call: true, update: false, result: true, normalizedMetadata: true, diffs: true, changedFiles: false, workspaceGuardrails: workspace_guardrails_enabled?, focusedContext: true, contextBudgetStats: true },
347
358
  errors: true,
348
359
  sessionUpdates: false
349
360
  },
@@ -384,9 +395,11 @@ module Kward
384
395
  logout: true
385
396
  },
386
397
  memory: { supported: true, optIn: true, defaultEnabled: false, autoSummaryDefaultEnabled: false, promptInjection: "interactive", storage: { core: "json", soft: "jsonl", events: "jsonl" }, methods: MEMORY_METHODS },
398
+ workers: workers_capability,
387
399
  commands: { supported: true, methods: ["commands/list", "commands/run"], method: "commands/list", runMethod: "commands/run", sources: ["builtin", "prompt", "skill", "plugin"], executableSources: ["builtin", "plugin"] },
388
400
  startupResources: { supported: true, method: "resources/startup" },
389
401
  starterPack: { supported: false, reason: "cliOnlyInstallCommand" },
402
+ shell: { supported: false, reason: "interactiveTuiOnly" },
390
403
  extensionUi: {
391
404
  question: { supported: true, notification: "ui/question", method: "ui/answerQuestion", maxQuestions: 4, multiSelect: false, preview: false },
392
405
  select: false,
@@ -426,6 +439,18 @@ module Kward
426
439
  }
427
440
  end
428
441
 
442
+ def workers_capability
443
+ return { supported: false, reason: "experimentalWorkersFlagRequired", flag: "--experimental-workers" } unless @experimental_workers
444
+
445
+ { supported: true, methods: WORKER_METHODS, roles: ["implementation", "request"], statuses: Workers::Worker::STATUSES, transcriptStorage: "sessions", metadataStorage: "json" }
446
+ end
447
+
448
+ def require_experimental_workers!
449
+ return if @experimental_workers
450
+
451
+ raise NoMethodError, "workers require --experimental-workers"
452
+ end
453
+
429
454
  def workspace_info(root)
430
455
  root = @session_manager.validate_workspace_root(root)
431
456
  { root: root, basename: File.basename(root), writable: File.writable?(root) }
@@ -571,6 +596,20 @@ module Kward
571
596
  { sections: sections }
572
597
  end
573
598
 
599
+ def workers_list(params)
600
+ include_archived = params["includeArchived"] == true
601
+ workers = @worker_store.list(include_archived: include_archived)
602
+ { workers: workers }
603
+ end
604
+
605
+ def workers_show(params)
606
+ id = params.fetch("id").to_s.delete_prefix("#")
607
+ worker = @worker_store.find(id)
608
+ return { worker: worker } if worker
609
+
610
+ raise ArgumentError, "Unknown worker: #{id}"
611
+ end
612
+
574
613
  def auth_login_with_api_key(params)
575
614
  result = @auth_manager.login_with_api_key(provider_id: params.fetch("providerId"), api_key: params.fetch("apiKey"))
576
615
  @session_manager.refresh_client_config