kward 0.71.0 → 0.73.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 (143) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +30 -0
  3. data/CHANGELOG.md +93 -0
  4. data/Gemfile.lock +2 -2
  5. data/README.md +4 -0
  6. data/doc/agent-tools.md +15 -6
  7. data/doc/authentication.md +22 -1
  8. data/doc/code-search.md +42 -2
  9. data/doc/configuration.md +106 -3
  10. data/doc/context-budgeting.md +136 -0
  11. data/doc/context-tools.md +16 -3
  12. data/doc/editor.md +415 -0
  13. data/doc/extensibility.md +16 -7
  14. data/doc/files.md +100 -0
  15. data/doc/getting-started.md +25 -18
  16. data/doc/git.md +123 -0
  17. data/doc/memory.md +24 -4
  18. data/doc/personas.md +34 -5
  19. data/doc/plugins.md +72 -1
  20. data/doc/releasing.md +37 -9
  21. data/doc/rpc.md +75 -5
  22. data/doc/session-management.md +35 -1
  23. data/doc/shell.md +332 -0
  24. data/doc/tabs.md +122 -0
  25. data/doc/troubleshooting.md +77 -1
  26. data/doc/usage.md +79 -7
  27. data/doc/web-search.md +12 -4
  28. data/doc/workspace-tools.md +51 -12
  29. data/examples/plugins/space_invaders.rb +377 -0
  30. data/lib/kward/agent.rb +1 -1
  31. data/lib/kward/ansi.rb +62 -23
  32. data/lib/kward/cli/commands.rb +33 -2
  33. data/lib/kward/cli/git.rb +150 -0
  34. data/lib/kward/cli/interactive_turn.rb +73 -9
  35. data/lib/kward/cli/plugins.rb +54 -4
  36. data/lib/kward/cli/prompt_interface.rb +32 -1
  37. data/lib/kward/cli/rendering.rb +4 -1
  38. data/lib/kward/cli/runtime_helpers.rb +268 -4
  39. data/lib/kward/cli/sessions.rb +2 -2
  40. data/lib/kward/cli/settings.rb +217 -9
  41. data/lib/kward/cli/slash_commands.rb +628 -2
  42. data/lib/kward/cli/tabs.rb +725 -0
  43. data/lib/kward/cli/tool_summaries.rb +6 -0
  44. data/lib/kward/cli.rb +150 -26
  45. data/lib/kward/clipboard.rb +2 -3
  46. data/lib/kward/compactor.rb +7 -19
  47. data/lib/kward/config_files.rb +145 -1
  48. data/lib/kward/context_budget_meter.rb +44 -0
  49. data/lib/kward/conversation.rb +12 -4
  50. data/lib/kward/editor_mode.rb +25 -0
  51. data/lib/kward/ekwsh.rb +559 -0
  52. data/lib/kward/image_attachments.rb +3 -1
  53. data/lib/kward/interactive_pty_runner.rb +151 -0
  54. data/lib/kward/local_command_runner.rb +155 -0
  55. data/lib/kward/local_pty_command_runner.rb +171 -0
  56. data/lib/kward/model/context_usage.rb +2 -2
  57. data/lib/kward/model/payloads.rb +2 -5
  58. data/lib/kward/plugin_registry.rb +61 -0
  59. data/lib/kward/project_files.rb +52 -0
  60. data/lib/kward/prompt_history.rb +84 -0
  61. data/lib/kward/prompt_interface/composer_controller.rb +69 -1
  62. data/lib/kward/prompt_interface/composer_renderer.rb +109 -13
  63. data/lib/kward/prompt_interface/composer_state.rb +96 -27
  64. data/lib/kward/prompt_interface/editor/auto_close_pairs.rb +123 -0
  65. data/lib/kward/prompt_interface/editor/auto_indent.rb +510 -0
  66. data/lib/kward/prompt_interface/editor/buffer.rb +109 -0
  67. data/lib/kward/prompt_interface/editor/controller.rb +1218 -0
  68. data/lib/kward/prompt_interface/editor/endwise.rb +321 -0
  69. data/lib/kward/prompt_interface/editor/file_marker.rb +40 -0
  70. data/lib/kward/prompt_interface/editor/indent_navigation.rb +61 -0
  71. data/lib/kward/prompt_interface/editor/kill_ring.rb +78 -0
  72. data/lib/kward/prompt_interface/editor/modes/emacs.rb +259 -0
  73. data/lib/kward/prompt_interface/editor/modes/modern.rb +354 -0
  74. data/lib/kward/prompt_interface/editor/modes/vibe.rb +1812 -0
  75. data/lib/kward/prompt_interface/editor/modes/vibe_insert_readline.rb +166 -0
  76. data/lib/kward/prompt_interface/editor/renderer.rb +244 -0
  77. data/lib/kward/prompt_interface/editor/search.rb +76 -0
  78. data/lib/kward/prompt_interface/editor/selections.rb +120 -0
  79. data/lib/kward/prompt_interface/editor/state.rb +1271 -0
  80. data/lib/kward/prompt_interface/editor/status_text.rb +23 -0
  81. data/lib/kward/prompt_interface/editor/syntax_highlighter.rb +422 -0
  82. data/lib/kward/prompt_interface/editor/undo_history.rb +46 -0
  83. data/lib/kward/prompt_interface/editor/vibe_state.rb +44 -0
  84. data/lib/kward/prompt_interface/file_overlay.rb +211 -0
  85. data/lib/kward/prompt_interface/git_prompt.rb +288 -0
  86. data/lib/kward/prompt_interface/interactive/controller.rb +186 -0
  87. data/lib/kward/prompt_interface/interactive/renderer.rb +71 -0
  88. data/lib/kward/prompt_interface/interactive/state.rb +62 -0
  89. data/lib/kward/prompt_interface/key_handler.rb +451 -57
  90. data/lib/kward/prompt_interface/overlay_renderer.rb +21 -2
  91. data/lib/kward/prompt_interface/project_browser.rb +524 -0
  92. data/lib/kward/prompt_interface/question_prompt.rb +99 -56
  93. data/lib/kward/prompt_interface/runtime_state.rb +43 -0
  94. data/lib/kward/prompt_interface/screen.rb +19 -3
  95. data/lib/kward/prompt_interface/selection_prompt.rb +10 -19
  96. data/lib/kward/prompt_interface/slash_overlay.rb +2 -0
  97. data/lib/kward/prompt_interface/stream_state.rb +7 -0
  98. data/lib/kward/prompt_interface/transcript_buffer.rb +6 -0
  99. data/lib/kward/prompt_interface.rb +366 -222
  100. data/lib/kward/prompts/commands.rb +9 -0
  101. data/lib/kward/prompts.rb +2 -0
  102. data/lib/kward/rpc/memory_methods.rb +83 -0
  103. data/lib/kward/rpc/server.rb +169 -83
  104. data/lib/kward/rpc/session_manager.rb +45 -121
  105. data/lib/kward/rpc/session_tree_rows.rb +9 -115
  106. data/lib/kward/rpc/tool_event_normalizer.rb +1 -1
  107. data/lib/kward/rpc/tool_metadata.rb +11 -0
  108. data/lib/kward/rpc/transcript_normalizer.rb +4 -39
  109. data/lib/kward/scratchpad_runner.rb +56 -0
  110. data/lib/kward/session_diff.rb +20 -3
  111. data/lib/kward/session_naming.rb +11 -0
  112. data/lib/kward/session_store.rb +44 -0
  113. data/lib/kward/session_tree_nodes.rb +136 -0
  114. data/lib/kward/session_tree_renderer.rb +9 -131
  115. data/lib/kward/tab_store.rb +47 -0
  116. data/lib/kward/terminal_keys.rb +84 -0
  117. data/lib/kward/terminal_sequences.rb +42 -0
  118. data/lib/kward/text_boundary.rb +25 -0
  119. data/lib/kward/tools/context_budget_stats.rb +54 -0
  120. data/lib/kward/tools/context_for_task.rb +204 -0
  121. data/lib/kward/tools/read_file.rb +8 -4
  122. data/lib/kward/tools/registry.rb +62 -16
  123. data/lib/kward/tools/tool_call.rb +10 -0
  124. data/lib/kward/version.rb +1 -1
  125. data/lib/kward/workers/git_guard.rb +93 -0
  126. data/lib/kward/workers/job.rb +99 -0
  127. data/lib/kward/workers/live_view.rb +49 -0
  128. data/lib/kward/workers/manager.rb +288 -0
  129. data/lib/kward/workers/queue_runner.rb +166 -0
  130. data/lib/kward/workers/queue_store.rb +112 -0
  131. data/lib/kward/workers/store.rb +72 -0
  132. data/lib/kward/workers/tool_policy.rb +23 -0
  133. data/lib/kward/workers/worker.rb +82 -0
  134. data/lib/kward/workers/write_lock.rb +38 -0
  135. data/lib/kward/workers.rb +10 -0
  136. data/lib/kward/workspace.rb +125 -87
  137. data/templates/default/fulldoc/html/css/kward.css +140 -36
  138. data/templates/default/fulldoc/html/images/kward_screen_1.png +0 -0
  139. data/templates/default/fulldoc/html/setup.rb +1 -0
  140. data/templates/default/kward_navigation.rb +12 -1
  141. data/templates/default/layout/html/layout.erb +23 -34
  142. data/templates/default/layout/html/setup.rb +6 -0
  143. metadata +67 -1
@@ -1,21 +1,46 @@
1
+ require "find"
1
2
  require "io/console"
3
+ require "pathname"
4
+ require "rbconfig"
2
5
  require "thread"
6
+ require_relative "project_files"
7
+ require_relative "prompt_history"
3
8
  require "tty-cursor"
4
9
  require "tty-reader"
5
10
  require "tty-screen"
6
11
  require_relative "ansi"
12
+ require_relative "terminal_sequences"
13
+ require_relative "terminal_keys"
14
+ require_relative "editor_mode"
7
15
  require_relative "prompt_interface/banner"
8
16
  require_relative "prompt_interface/composer_state"
17
+ require_relative "prompt_interface/editor/state"
9
18
  require_relative "prompt_interface/transcript_buffer"
10
19
  require_relative "prompt_interface/transcript_renderer"
11
20
  require_relative "prompt_interface/prompt_renderer"
12
21
  require_relative "prompt_interface/stream_state"
13
22
  require_relative "prompt_interface/slash_overlay"
23
+ require_relative "prompt_interface/file_overlay"
24
+ require_relative "prompt_interface/project_browser"
14
25
  require_relative "prompt_interface/selection_prompt"
15
26
  require_relative "prompt_interface/question_prompt"
27
+ require_relative "prompt_interface/git_prompt"
16
28
  require_relative "prompt_interface/overlay_renderer"
29
+ require_relative "prompt_interface/editor/renderer"
30
+ require_relative "prompt_interface/editor/syntax_highlighter"
31
+ require_relative "prompt_interface/editor/auto_close_pairs"
32
+ require_relative "prompt_interface/editor/endwise"
33
+ require_relative "prompt_interface/editor/auto_indent"
17
34
  require_relative "prompt_interface/composer_renderer"
18
35
  require_relative "prompt_interface/composer_controller"
36
+ require_relative "prompt_interface/editor/modes/modern"
37
+ require_relative "prompt_interface/editor/modes/emacs"
38
+ require_relative "prompt_interface/editor/modes/vibe_insert_readline"
39
+ require_relative "prompt_interface/editor/modes/vibe"
40
+ require_relative "prompt_interface/editor/controller"
41
+ require_relative "prompt_interface/interactive/controller"
42
+ require_relative "prompt_interface/interactive/renderer"
43
+ require_relative "prompt_interface/interactive/state"
19
44
  require_relative "prompt_interface/layout"
20
45
  require_relative "prompt_interface/screen"
21
46
  require_relative "prompt_interface/key_handler"
@@ -39,33 +64,51 @@ module Kward
39
64
  SPINNER_FRAMES = %w[⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏].freeze
40
65
  SPINNER_INTERVAL = 0.1
41
66
  FOOTER_REFRESH_INTERVAL = 1.0
67
+ COMPOSER_STATUS_REFRESH_INTERVAL = 1.0
42
68
  COMPOSER_MAX_INPUT_ROWS = 6
43
69
  TRANSCRIPT_BUFFER_LIMIT = 200_000
44
70
  BANNER_MESSAGE = Banner::MESSAGE
45
71
 
46
72
  include SlashOverlay
73
+ include FileOverlay
74
+ include ProjectBrowser
47
75
  include SelectionPrompt
48
76
  include QuestionPrompt
77
+ include GitPrompt
49
78
  include OverlayRenderer
79
+ include EditorRenderer
80
+ include EditorSyntaxHighlighter
81
+ include EditorAutoClosePairs
82
+ include EditorEndwise
83
+ include EditorAutoIndent
50
84
  include ComposerRenderer
51
85
  include ComposerController
86
+ include ModernEditorMode
87
+ include EmacsEditorMode
88
+ include VibeInsertReadline
89
+ include VibeEditorMode
90
+ include EditorController
91
+ include InteractiveRenderer
92
+ include InteractiveState
52
93
  include Layout
53
94
  include Screen
54
95
  include KeyHandler
55
96
  include RuntimeState
56
97
  include TranscriptRenderer
57
98
  include PromptRenderer
58
- KEYBOARD_PROTOCOL_ENABLE = "\e[>1u".freeze
59
- KEYBOARD_PROTOCOL_RESTORE = "\e[<u".freeze
60
- BRACKETED_PASTE_ENABLE = "\e[?2004h".freeze
61
- BRACKETED_PASTE_RESTORE = "\e[?2004l".freeze
62
- BRACKETED_PASTE_START = "\e[200~".freeze
63
- BRACKETED_PASTE_END = "\e[201~".freeze
64
- SYNCHRONIZED_OUTPUT_ENABLE = "\e[?2026h".freeze
65
- SYNCHRONIZED_OUTPUT_DISABLE = "\e[?2026l".freeze
66
- CURSOR_SHOW = "\e[?25h".freeze
67
- CURSOR_HIDE = "\e[?25l".freeze
68
- SHIFT_ENTER_SEQUENCES = ["\e[13;2u", "\e[13;2~", "\e[27;2;13~", "\e\r", "\e\n"].freeze
99
+ KEYBOARD_PROTOCOL_ENABLE = TerminalSequences::KEYBOARD_PROTOCOL_ENABLE
100
+ KEYBOARD_PROTOCOL_RESTORE = TerminalSequences::KEYBOARD_PROTOCOL_RESTORE
101
+ BRACKETED_PASTE_ENABLE = TerminalSequences::BRACKETED_PASTE_ENABLE
102
+ BRACKETED_PASTE_RESTORE = TerminalSequences::BRACKETED_PASTE_RESTORE
103
+ BRACKETED_PASTE_START = TerminalSequences::BRACKETED_PASTE_START
104
+ BRACKETED_PASTE_END = TerminalSequences::BRACKETED_PASTE_END
105
+ SYNCHRONIZED_OUTPUT_ENABLE = TerminalSequences::SYNCHRONIZED_OUTPUT_ENABLE
106
+ SYNCHRONIZED_OUTPUT_DISABLE = TerminalSequences::SYNCHRONIZED_OUTPUT_DISABLE
107
+ CURSOR_SHOW = TerminalSequences::CURSOR_SHOW
108
+ CURSOR_HIDE = TerminalSequences::CURSOR_HIDE
109
+ CURSOR_SHAPE_DEFAULT = TerminalSequences::CURSOR_SHAPE_DEFAULT
110
+ CURSOR_SHAPE_BAR = TerminalSequences::CURSOR_SHAPE_BAR
111
+ SHIFT_ENTER_SEQUENCES = TerminalKeys::SHIFT_ENTER
69
112
  EXIT_INPUT = :exit_input
70
113
  CANCEL_INPUT = :cancel_input
71
114
  SELECT_CANCEL = :select_cancel
@@ -82,12 +125,14 @@ module Kward
82
125
  end
83
126
  end
84
127
 
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)
128
+ 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
129
  @input_io = input
87
130
  @output_io = output
88
131
  @reader = TTY::Reader.new(input: input, output: output, interrupt: :error)
89
132
  @mutex = Mutex.new
133
+ @prompt_history = prompt_history
90
134
  @composer = ComposerState.new
135
+ load_history(@prompt_history.values) if @prompt_history
91
136
  self.composer_input = @composer.input
92
137
  self.composer_cursor = @composer.cursor
93
138
  @started = false
@@ -99,6 +144,8 @@ module Kward
99
144
  @spinner_frame_index = 0
100
145
  @last_spinner_tick = monotonic_now
101
146
  @last_footer_refresh = monotonic_now
147
+ @last_composer_status_refresh = 0.0
148
+ @cached_composer_status_text = nil
102
149
  @prompt_label = "You>"
103
150
  @assistant_label = "Assistant"
104
151
  @stream_state = StreamState.new
@@ -109,18 +156,33 @@ module Kward
109
156
  @transcript_viewport_rows = 0
110
157
  @restoring_transcript = false
111
158
  @pending_keys = []
159
+ @completion_provider = nil
112
160
  @original_console_mode = nil
113
161
  @raw_mode_active = false
114
162
  @slash_commands = normalize_slash_commands(slash_commands)
115
163
  @slash_selection_index = 0
116
164
  @slash_overlay_dismissed_input = nil
165
+ @slash_overlay_disabled = false
166
+ @file_selection_index = 0
167
+ @file_overlay_dismissed_token = nil
168
+ @file_open_dismissed_token = nil
169
+ @file_editor_open_status = nil
170
+ @file_mention_paths = nil
171
+ @project_browser_state = nil
172
+ @project_browser_restore_after_editor = false
173
+ @editor_state = nil
174
+ @interactive_state = nil
175
+ @last_interactive_tick = monotonic_now
117
176
  @select_state = nil
118
177
  @question_state = nil
178
+ @question_prompt_active = false
179
+ @git_state = nil
119
180
  @last_width = screen_width
120
181
  @last_height = screen_height
121
182
  @reserved_rows = 0
122
183
  @color_enabled = ANSI.enabled?(output)
123
184
  @cursor_visible = true
185
+ @editor_bar_cursor_active = false
124
186
  @synchronized_output_depth = 0
125
187
  @overlay_settings = normalize_overlay_settings(overlay_settings)
126
188
  @footer = footer
@@ -129,6 +191,21 @@ module Kward
129
191
  @attachment_badges = attachment_badges
130
192
  @attachment_parser = attachment_parser
131
193
  @banner = Banner.new(message: banner_message, screen_height: method(:screen_height))
194
+ @tabs = []
195
+ @active_tab_index = 0
196
+ @tab_keybindings = normalize_tab_keybindings(tab_keybindings)
197
+ @editor_mode = normalize_editor_mode(editor_mode)
198
+ @editor_mode_source = editor_mode_source
199
+ @editor_auto_indent = editor_auto_indent != false
200
+ @editor_auto_indent_source = editor_auto_indent_source
201
+ @editor_auto_close_pairs = editor_auto_close_pairs != false
202
+ @editor_auto_close_pairs_source = editor_auto_close_pairs_source
203
+ @editor_soft_wrap = editor_soft_wrap != false
204
+ @editor_soft_wrap_source = editor_soft_wrap_source
205
+ @editor_bar_cursor = editor_bar_cursor != false
206
+ @editor_bar_cursor_source = editor_bar_cursor_source
207
+ @editor_line_numbers = normalize_editor_line_numbers(editor_line_numbers)
208
+ @editor_line_numbers_source = editor_line_numbers_source
132
209
  end
133
210
 
134
211
  def start(render: true)
@@ -138,6 +215,7 @@ module Kward
138
215
  enter_raw_mode_locked
139
216
  @started = true
140
217
  @asking = true
218
+ disable_editor_mouse_reporting(force: true) unless editor_active?
141
219
  @output_io.print(KEYBOARD_PROTOCOL_ENABLE)
142
220
  @output_io.print(BRACKETED_PASTE_ENABLE)
143
221
  render_prompt_locked if render
@@ -150,8 +228,10 @@ module Kward
150
228
 
151
229
  clear_prompt_for_output_locked
152
230
  restore_scroll_region_locked
231
+ disable_editor_mouse_reporting(force: true)
153
232
  @output_io.print(BRACKETED_PASTE_RESTORE)
154
233
  @output_io.print(KEYBOARD_PROTOCOL_RESTORE)
234
+ restore_editor_cursor_shape_locked
155
235
  set_cursor_visible_locked(true, force: true)
156
236
  @output_io.puts
157
237
  @output_io.flush
@@ -220,6 +300,79 @@ module Kward
220
300
  end
221
301
  end
222
302
 
303
+ def with_completion_provider(provider, slash_overlay: true)
304
+ previous_provider = @completion_provider
305
+ previous_slash_overlay_disabled = @slash_overlay_disabled
306
+ @completion_provider = provider
307
+ @slash_overlay_disabled = !slash_overlay
308
+ yield
309
+ ensure
310
+ @completion_provider = previous_provider
311
+ @slash_overlay_disabled = previous_slash_overlay_disabled
312
+ end
313
+
314
+ def with_prompt_history(history)
315
+ previous_history = @prompt_history
316
+ @prompt_history = history
317
+ load_history(@prompt_history.values) if @prompt_history
318
+ yield
319
+ ensure
320
+ @prompt_history = previous_history
321
+ load_history(@prompt_history.values) if @prompt_history
322
+ end
323
+
324
+ def editing_file?
325
+ @mutex.synchronize { editor_active? }
326
+ end
327
+
328
+ def edit_file(path, base_dir: Dir.pwd, allow_new: true)
329
+ start(render: false)
330
+ opened = @mutex.synchronize do
331
+ open_editor(path, allow_new: allow_new, base_dir: base_dir, restrict_to_workspace: false).tap do
332
+ render_prompt_locked
333
+ end
334
+ end
335
+ return false unless opened
336
+
337
+ run_editor
338
+ end
339
+
340
+ def scratchpad(language = :text)
341
+ start(render: false)
342
+ opened = @mutex.synchronize do
343
+ open_scratchpad(language).tap do
344
+ render_prompt_locked
345
+ end
346
+ end
347
+ return false unless opened
348
+
349
+ run_editor
350
+ end
351
+
352
+ def run_editor
353
+ loop do
354
+ key = read_key(nonblock: true)
355
+ action = nil
356
+ editor_open = @mutex.synchronize do
357
+ if key.nil?
358
+ resized = handle_resize_locked
359
+ footer_refreshed = tick_footer_locked
360
+ render_prompt_locked if resized || footer_refreshed
361
+ else
362
+ result = handle_key(key)
363
+ action = result if prompt_action_result?(result)
364
+ render_prompt_locked unless result.is_a?(String) || result == EXIT_INPUT || prompt_action_result?(result)
365
+ end
366
+ editor_active?
367
+ end
368
+ return action if action
369
+ break unless editor_open
370
+
371
+ sleep 0.02 if key.nil?
372
+ end
373
+ true
374
+ end
375
+
223
376
  def ask(message = "You>")
224
377
  was_composing = @started && @asking
225
378
  start
@@ -250,10 +403,10 @@ module Kward
250
403
  render_prompt_locked if resized || footer_refreshed
251
404
  else
252
405
  result = handle_key(key)
253
- render_prompt_locked unless result.is_a?(String) || result == EXIT_INPUT
406
+ render_prompt_locked unless result.is_a?(String) || result == EXIT_INPUT || prompt_action_result?(result)
254
407
  end
255
408
  end
256
- return result if result.is_a?(String)
409
+ return result if result.is_a?(String) || prompt_action_result?(result)
257
410
  return nil if result == EXIT_INPUT
258
411
 
259
412
  sleep 0.02 if key.nil?
@@ -323,7 +476,10 @@ module Kward
323
476
  start
324
477
  saved_state = nil
325
478
  answers = []
326
- @mutex.synchronize { saved_state = begin_question_prompt_state }
479
+ @mutex.synchronize do
480
+ @question_prompt_active = true
481
+ saved_state = begin_question_prompt_state
482
+ end
327
483
 
328
484
  questions.each_with_index do |question, index|
329
485
  answer = ask_single_user_question(question, index + 1, questions.length)
@@ -342,7 +498,157 @@ module Kward
342
498
  end
343
499
 
344
500
  def modal_active?
345
- @mutex.synchronize { !@question_state.nil? || !@select_state.nil? }
501
+ @mutex.synchronize { modal_active_locked? }
502
+ end
503
+
504
+ def interactive_active?
505
+ @mutex.synchronize { interactive_active_locked? }
506
+ end
507
+
508
+ def interactive_exited?
509
+ @mutex.synchronize do
510
+ return false unless @interactive_state
511
+
512
+ @interactive_state[:controller].exited?
513
+ end
514
+ end
515
+
516
+ def finish_interactive
517
+ @mutex.synchronize do
518
+ return unless @interactive_state
519
+
520
+ snapshot = @interactive_state[:snapshot]
521
+ @interactive_state = nil
522
+ restore_composer_snapshot_locked(snapshot)
523
+ redraw_screen_locked if @started
524
+ @output_io.flush
525
+ end
526
+ end
527
+
528
+ def with_terminal_handoff
529
+ start
530
+ input = nil
531
+ output = nil
532
+ @mutex.synchronize do
533
+ clear_prompt_for_output_locked
534
+ restore_scroll_region_locked
535
+ disable_editor_mouse_reporting(force: true)
536
+ @output_io.print(BRACKETED_PASTE_RESTORE)
537
+ @output_io.print(KEYBOARD_PROTOCOL_RESTORE)
538
+ restore_editor_cursor_shape_locked
539
+ set_cursor_visible_locked(true, force: true)
540
+ @output_io.flush
541
+ restore_console_mode_locked
542
+ input = @input_io
543
+ output = @output_io
544
+ end
545
+
546
+ yield(input, output)
547
+ ensure
548
+ @mutex.synchronize do
549
+ enter_raw_mode_locked
550
+ @output_io.print(KEYBOARD_PROTOCOL_ENABLE)
551
+ @output_io.print(BRACKETED_PASTE_ENABLE)
552
+ @last_composer_rows = []
553
+ render_prompt_locked if @started && @asking
554
+ @output_io.flush
555
+ end
556
+ end
557
+
558
+ def start_interactive(title:, rows:, fps:)
559
+ snapshot = composer_snapshot
560
+ controller = InteractiveController.new(width: interactive_canvas_width, height: rows, fps: fps)
561
+ start
562
+ @mutex.synchronize do
563
+ @interactive_state = {
564
+ title: title.to_s,
565
+ rows: rows,
566
+ controller: controller,
567
+ snapshot: snapshot
568
+ }
569
+ @last_interactive_tick = monotonic_now
570
+ @asking = true
571
+ @busy = false
572
+ @last_composer_rows = []
573
+ render_prompt_locked
574
+ end
575
+ controller
576
+ end
577
+
578
+ def update_tabs(labels:, active_index: 0)
579
+ @mutex.synchronize do
580
+ @tabs = Array(labels).map { |label| normalize_tab_label(label) }
581
+ @active_tab_index = active_index.to_i
582
+ render_prompt_locked if @started && @asking
583
+ end
584
+ end
585
+
586
+ def composer_snapshot
587
+ @mutex.synchronize do
588
+ {
589
+ composer: @composer,
590
+ prompt_label: @prompt_label
591
+ }
592
+ end
593
+ end
594
+
595
+ def tab_view_snapshot
596
+ @mutex.synchronize do
597
+ {
598
+ composer: @composer.dup,
599
+ prompt_label: @prompt_label.dup,
600
+ editor_state: @editor_state&.dup,
601
+ transcript_buffer: @transcript_buffer.dup,
602
+ transcript_viewport_rows: @transcript_viewport_rows,
603
+ stream_state: @stream_state.dup
604
+ }
605
+ end
606
+ end
607
+
608
+ def restore_composer_snapshot(snapshot)
609
+ @mutex.synchronize do
610
+ restore_composer_snapshot_locked(snapshot)
611
+ restore_editor_snapshot_locked(snapshot)
612
+ redraw_screen_locked if @started
613
+ end
614
+ end
615
+
616
+ def restore_tab_view_snapshot(snapshot)
617
+ @mutex.synchronize do
618
+ restore_composer_snapshot_locked(snapshot)
619
+ restore_editor_snapshot_locked(snapshot)
620
+ @transcript_buffer = snapshot[:transcript_buffer] || TranscriptBuffer.new(limit: TRANSCRIPT_BUFFER_LIMIT)
621
+ @transcript_viewport_rows = snapshot[:transcript_viewport_rows].to_i
622
+ @stream_state = snapshot[:stream_state] || StreamState.new
623
+ @last_composer_rows = []
624
+ redraw_screen_locked if @started
625
+ end
626
+ end
627
+
628
+ def restore_composer_snapshot_locked(snapshot)
629
+ @composer = snapshot[:composer] || new_composer_state_with_history
630
+ @prompt_label = snapshot[:prompt_label].to_s.empty? ? "You>" : snapshot[:prompt_label].to_s
631
+ self.composer_input = @composer.input
632
+ self.composer_cursor = @composer.cursor
633
+ @last_composer_rows = []
634
+ end
635
+
636
+ def new_composer_state_with_history
637
+ composer = ComposerState.new
638
+ composer.load_history(@prompt_history.values) if @prompt_history
639
+ composer
640
+ end
641
+
642
+ def restore_editor_snapshot_locked(snapshot)
643
+ editor_was_active = editor_active?
644
+ @editor_state = snapshot[:editor_state]&.dup
645
+ editor_is_active = editor_active?
646
+
647
+ if editor_is_active
648
+ enable_editor_mouse_reporting unless editor_was_active
649
+ else
650
+ disable_editor_mouse_reporting(force: true)
651
+ end
346
652
  end
347
653
 
348
654
  def update_overlay_settings(settings)
@@ -409,6 +715,22 @@ module Kward
409
715
  def poll_input
410
716
  key = read_key(nonblock: true)
411
717
  @mutex.synchronize do
718
+ if interactive_active_locked?
719
+ if key.nil?
720
+ resized = handle_resize_locked
721
+ ticked = tick_interactive_locked
722
+ render_prompt_locked if resized || ticked
723
+ return :interactive_exited if @interactive_state[:controller].exited?
724
+ return nil
725
+ end
726
+
727
+ route_interactive_key(key)
728
+ ticked = tick_interactive_locked
729
+ render_prompt_locked if ticked
730
+ return :interactive_exited if @interactive_state[:controller].exited?
731
+ return nil
732
+ end
733
+
412
734
  if key.nil?
413
735
  resized = handle_resize_locked
414
736
  spun = tick_spinner_locked
@@ -417,8 +739,13 @@ module Kward
417
739
  return nil
418
740
  end
419
741
 
742
+ if modal_active_locked?
743
+ queue_pending_keys(key)
744
+ return nil
745
+ end
746
+
420
747
  result = handle_key(key)
421
- render_prompt_locked unless [EXIT_INPUT, CANCEL_INPUT].include?(result)
748
+ render_prompt_locked unless [EXIT_INPUT, CANCEL_INPUT].include?(result) || prompt_action_result?(result)
422
749
  [EXIT_INPUT, CANCEL_INPUT].include?(result) ? result : result
423
750
  end
424
751
  end
@@ -465,6 +792,17 @@ module Kward
465
792
  end
466
793
  end
467
794
 
795
+ def write_transcript_delta(delta)
796
+ @mutex.synchronize do
797
+ with_synchronized_output_locked do
798
+ prepare_transcript_output_locked unless @restoring_transcript
799
+ write_transcript_text_locked(delta.to_s)
800
+ restore_composer_cursor_locked unless @restoring_transcript
801
+ end
802
+ @output_io.flush unless @restoring_transcript
803
+ end
804
+ end
805
+
468
806
  def write_stream_block(label, delta, finish: false)
469
807
  @mutex.synchronize do
470
808
  write_stream_block_locked(label, delta.to_s, finish: finish)
@@ -479,6 +817,14 @@ module Kward
479
817
  end
480
818
  end
481
819
 
820
+ def refresh_composer_status
821
+ @mutex.synchronize do
822
+ @cached_composer_status_text = nil
823
+ @last_composer_status_refresh = 0.0
824
+ render_prompt_locked if @started && @asking
825
+ end
826
+ end
827
+
482
828
  def clear_transcript
483
829
  @mutex.synchronize do
484
830
  @transcript_buffer.clear
@@ -493,210 +839,8 @@ module Kward
493
839
 
494
840
  private
495
841
 
496
-
497
-
498
-
499
-
500
-
501
-
502
-
503
-
504
-
505
-
506
-
507
-
508
-
509
-
510
-
511
-
512
-
513
-
514
-
515
-
516
-
517
-
518
-
519
-
520
-
521
-
522
-
523
-
524
-
525
-
526
-
527
-
528
-
529
-
530
-
531
-
532
-
533
-
534
-
535
-
536
-
537
-
538
-
539
-
540
-
541
-
542
-
543
-
544
-
545
-
546
-
547
-
548
-
549
-
550
-
551
-
552
-
553
-
554
-
555
-
556
-
557
-
558
-
559
-
560
-
561
-
562
-
563
-
564
-
565
-
566
-
567
-
568
-
569
-
570
-
571
-
572
-
573
-
574
-
575
-
576
-
577
-
578
-
579
-
580
-
581
-
582
-
583
-
584
-
585
-
586
-
587
-
588
-
589
-
590
-
591
-
592
-
593
-
594
-
595
-
596
-
597
-
598
-
599
-
600
-
601
-
602
-
603
-
604
-
605
-
606
-
607
-
608
-
609
-
610
-
611
-
612
-
613
-
614
-
615
-
616
-
617
-
618
-
619
-
620
-
621
-
622
-
623
-
624
-
625
-
626
-
627
-
628
-
629
-
630
-
631
-
632
-
633
-
634
-
635
-
636
-
637
-
638
-
639
-
640
-
641
-
642
-
643
-
644
-
645
-
646
-
647
-
648
-
649
-
650
-
651
-
652
-
653
-
654
-
655
-
656
-
657
-
658
-
659
-
660
-
661
-
662
-
663
-
664
-
665
-
666
-
667
-
668
-
669
-
670
-
671
-
672
-
673
-
674
-
675
-
676
-
677
-
678
-
679
-
680
-
681
-
682
-
683
-
684
-
685
-
686
-
687
-
688
-
689
-
690
-
691
-
692
-
693
-
694
-
695
-
696
-
697
-
698
-
699
-
700
-
842
+ def modal_active_locked?
843
+ @question_prompt_active || !@question_state.nil? || !@select_state.nil? || !@git_state.nil?
844
+ end
701
845
  end
702
846
  end