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
@@ -65,6 +65,7 @@ module Kward
65
65
  def finish_question_prompt(saved_state)
66
66
  @mutex.synchronize do
67
67
  @question_state = nil
68
+ @question_prompt_active = false
68
69
  @select_state = saved_state[:select_state]
69
70
  @prompt_label = saved_state[:prompt_label]
70
71
  self.composer_input = saved_state[:input]
@@ -81,20 +82,17 @@ module Kward
81
82
 
82
83
  def handle_question_key(key)
83
84
  return if handle_question_bracketed_paste_key(key)
85
+ return if handle_question_shift_enter_key(key)
84
86
 
85
87
  csi_result = handle_question_csi_u_key(key)
86
88
  return csi_result unless csi_result == false
87
89
 
88
- if key.is_a?(String) && key.length > 1
89
- token = next_key_token(key)
90
- if token.length < key.length
91
- queue_pending_keys(key[token.length..])
92
- return handle_question_key(token)
93
- end
94
- end
90
+ return true if handle_bundled_key(key) { |token| handle_question_key(token) }
91
+
92
+ binding_result = handle_question_composer_key_binding(key)
93
+ return binding_result unless binding_result == false
95
94
 
96
- key_name = @reader.console.keys[key]
97
- case key_name
95
+ case key_name_for(key)
98
96
  when :return, :enter
99
97
  current_question_answer
100
98
  when :backspace
@@ -102,13 +100,13 @@ module Kward
102
100
  when :delete
103
101
  question_delete_at_cursor
104
102
  when :left
105
- @composer.move_cursor_left
103
+ move_cursor_left
106
104
  when :right
107
- @composer.move_cursor_right
105
+ move_cursor_right
108
106
  when :home
109
- @composer.move_to_start_of_line
107
+ move_to_start_of_line
110
108
  when :end
111
- @composer.move_to_end_of_line
109
+ move_to_end_of_line
112
110
  when :up
113
111
  question_previous_choice
114
112
  when :down
@@ -132,21 +130,45 @@ module Kward
132
130
  return false unless sequence
133
131
 
134
132
  code = sequence[:code]
133
+ modifier = sequence[:modifier]
135
134
  queue_pending_keys(sequence[:remaining]) if sequence[:remaining] && !sequence[:remaining].empty?
136
135
 
137
136
  case code
138
137
  when 13
139
- current_question_answer
138
+ if modifier == 2
139
+ question_insert_string("\n")
140
+ nil
141
+ else
142
+ current_question_answer
143
+ end
140
144
  when 27
141
145
  SELECT_CANCEL
142
146
  when 8, 127
143
- question_delete_before_cursor
147
+ alt_modifier?(modifier) ? question_delete_word_before_cursor : question_delete_before_cursor
144
148
  nil
145
149
  else
146
- false
150
+ modified_result = handle_question_modified_csi_u_key(code, modifier)
151
+ return modified_result unless modified_result == false
152
+
153
+ question_insert_csi_u_text(sequence)
147
154
  end
148
155
  end
149
156
 
157
+ def handle_question_modified_csi_u_key(code, modifier)
158
+ before = composer_input.dup
159
+ result = handle_modified_csi_u_key(code, modifier)
160
+ question_select_custom_choice if result != false && composer_input != before
161
+ result
162
+ end
163
+
164
+ def question_insert_csi_u_text(sequence)
165
+ text = csi_u_printable_text(sequence)
166
+ return true if text.nil? && csi_u_text_field?(sequence)
167
+ return false unless text
168
+
169
+ question_insert_string(text)
170
+ end
171
+
150
172
  def handle_question_escape_sequence
151
173
  pending_sequence = read_pending_escape_sequence
152
174
  return SELECT_CANCEL if pending_sequence.empty?
@@ -156,27 +178,24 @@ module Kward
156
178
  queue_pending_keys(full_sequence[sequence.length..]) if full_sequence.length > sequence.length
157
179
  return SELECT_CANCEL if sequence == "\e"
158
180
 
159
- key_name = @reader.console.keys[sequence]
160
- case key_name
181
+ binding_result = handle_question_composer_key_binding(sequence)
182
+ return binding_result unless binding_result == false
183
+
184
+ case key_name_for(sequence)
161
185
  when :up
162
186
  question_previous_choice
163
187
  when :down
164
188
  question_next_choice
165
189
  when :left
166
- @composer.move_cursor_left
190
+ move_cursor_left
167
191
  when :right
168
- @composer.move_cursor_right
192
+ move_cursor_right
169
193
  end
170
194
  true
171
195
  end
172
196
 
173
197
  def handle_question_bracketed_paste_key(key)
174
- paste = read_bracketed_paste(key)
175
- return false unless paste
176
-
177
- question_insert_string(normalize_paste(paste[:content]))
178
- queue_pending_keys(paste[:remaining]) if paste[:remaining] && !paste[:remaining].empty?
179
- true
198
+ handle_bracketed_paste(key) { |content| question_insert_string(content) }
180
199
  end
181
200
 
182
201
  def current_question_answer
@@ -244,34 +263,78 @@ module Kward
244
263
  question_insert_string(key)
245
264
  end
246
265
 
266
+ def handle_question_shift_enter_key(key)
267
+ sequence = shift_enter_sequence_for(key)
268
+ return false unless sequence
269
+
270
+ question_insert_string("\n")
271
+ queue_pending_keys(key[sequence.length..]) if key.length > sequence.length
272
+ true
273
+ end
274
+
275
+ def handle_question_composer_key_binding(key)
276
+ before = composer_input.dup
277
+ result = handle_composer_key_binding(key)
278
+ question_select_custom_choice if result != false && composer_input != before
279
+ result
280
+ end
281
+
247
282
  def question_insert_string(string)
248
283
  return if string.empty?
249
284
 
250
- @composer.insert_string(string)
251
- @question_state[:selection_index] = question_choices.length - 1 if @question_state
285
+ insert_string(string)
286
+ question_select_custom_choice
252
287
  end
253
288
 
254
289
  def question_delete_before_cursor
255
- return unless @composer.delete_before_cursor
256
-
257
- @question_state[:selection_index] = question_choices.length - 1 if @question_state && !composer_input.empty?
290
+ before = composer_input.dup
291
+ delete_before_cursor
292
+ question_select_custom_choice if composer_input != before && !composer_input.empty?
258
293
  end
259
294
 
260
295
  def question_delete_at_cursor
261
- return unless @composer.delete_at_cursor
296
+ before = composer_input.dup
297
+ delete_at_cursor
298
+ question_select_custom_choice if composer_input != before && !composer_input.empty?
299
+ end
262
300
 
263
- @question_state[:selection_index] = question_choices.length - 1 if @question_state && !composer_input.empty?
301
+ def question_delete_word_before_cursor
302
+ before = composer_input.dup
303
+ delete_word_before_cursor
304
+ question_select_custom_choice if composer_input != before && !composer_input.empty?
305
+ end
306
+
307
+ def question_select_custom_choice
308
+ @question_state[:selection_index] = question_choices.length - 1 if @question_state
264
309
  end
265
310
 
266
311
  def question_composer_layout(width, height = screen_height)
267
312
  content_width = [width - 4, 1].max
268
313
  overlay_rows = active_overlay_rows(width, height: height)
269
- rows = overlay_rows + [top_border(width), box_content_row("", content_width), bottom_border(width)]
270
- return [rows, question_custom_cursor_row, question_custom_cursor_col(width)] if selected_question_choice&.fetch(:custom, false)
314
+ return question_custom_composer_layout(width, height, overlay_rows, content_width) if selected_question_choice&.fetch(:custom, false)
271
315
 
316
+ rows = overlay_rows + [top_border(width), box_content_row("", content_width)]
317
+ rows.concat(question_bottom_border_rows(width))
272
318
  [rows, overlay_rows.length + 1, 2]
273
319
  end
274
320
 
321
+ def question_custom_composer_layout(width, height, overlay_rows, content_width)
322
+ input_layout_rows, input_cursor_row, input_cursor_col = input_layout(content_width)
323
+ max_input_rows = max_visible_input_rows(0, overlay_rows.length, 0, height: height)
324
+ visible_start = [[input_cursor_row - max_input_rows + 1, 0].max, [input_layout_rows.length - max_input_rows, 0].max].min
325
+ visible_rows = input_layout_rows[visible_start, max_input_rows] || [""]
326
+ rows = overlay_rows + [top_border(width)]
327
+ rows.concat(visible_rows.map { |row| box_content_row(row, content_width) })
328
+ rows.concat(question_bottom_border_rows(width))
329
+ cursor_row = overlay_rows.length + 1 + input_cursor_row - visible_start
330
+ cursor_col = 2 + [input_cursor_col, content_width - 1].min
331
+ [rows, cursor_row, cursor_col]
332
+ end
333
+
334
+ def question_bottom_border_rows(width)
335
+ @tabs.empty? ? [bottom_border(width)] : tab_border_rows(width)
336
+ end
337
+
275
338
  def question_overlay_rows(width)
276
339
  title = "Question #{@question_state[:index]}/#{@question_state[:total]} · #{@question_state[:header]}"
277
340
  lines = [
@@ -286,35 +349,15 @@ module Kward
286
349
  overlay_card_rows(title, lines, width)
287
350
  end
288
351
 
289
- def question_custom_cursor_row
290
- 4 + question_choices.index { |choice| choice[:custom] }.to_i
291
- end
292
-
293
- def question_custom_cursor_col(width)
294
- card_width = overlay_card_width(width)
295
- left_padding = overlay_left_padding(width, card_width)
296
- custom_prefix = selected_question_choice&.fetch(:custom, false) || !composer_input.empty? ? "Type something: " : "Type something."
297
- visible_before_cursor = display_question_input(composer_input[0...composer_cursor])
298
- [[left_padding + 2 + 2 + custom_prefix.length + visible_before_cursor.length, width - 1].min, 0].max
299
- end
300
-
301
352
  def choice_text(choice, selected: false)
302
353
  if choice[:custom]
303
- if selected || !composer_input.empty?
304
- "Type something: #{display_question_input(composer_input)}"
305
- else
306
- "Type something."
307
- end
354
+ selected ? "Type a custom answer below." : "Type something."
308
355
  else
309
356
  description = choice[:description].empty? ? "" : " — #{choice[:description]}"
310
357
  "#{choice[:label]}#{description}"
311
358
  end
312
359
  end
313
360
 
314
- def display_question_input(value)
315
- value.to_s.gsub(/\s+/, " ")
316
- end
317
-
318
361
  end
319
362
  end
320
363
  end
@@ -50,6 +50,21 @@ module Kward
50
50
  true
51
51
  end
52
52
 
53
+ def cached_composer_status_text
54
+ return nil unless @composer_status
55
+
56
+ now = monotonic_now
57
+ elapsed = now - @last_composer_status_refresh.to_f
58
+ if @cached_composer_status_text.nil? || elapsed >= COMPOSER_STATUS_REFRESH_INTERVAL
59
+ text = @composer_status.call.to_s
60
+ @cached_composer_status_text = text.empty? ? nil : status_composer_text(text)
61
+ @last_composer_status_refresh = now
62
+ end
63
+ @cached_composer_status_text
64
+ rescue StandardError
65
+ @cached_composer_status_text = nil
66
+ end
67
+
53
68
  def monotonic_now
54
69
  Process.clock_gettime(Process::CLOCK_MONOTONIC)
55
70
  end
@@ -59,6 +74,34 @@ module Kward
59
74
  ANSI.colorize(text, *styles, enabled: @color_enabled)
60
75
  end
61
76
 
77
+ def normalize_tab_keybindings(value)
78
+ text = value.to_s.downcase
79
+ return "ctrl" if text == "ctrl"
80
+ return "alt" if text == "alt"
81
+
82
+ RbConfig::CONFIG["host_os"].to_s.downcase.include?("darwin") ? "ctrl" : "alt"
83
+ end
84
+
85
+ def normalize_editor_mode(value)
86
+ EditorMode.normalize(value)
87
+ end
88
+
89
+ def normalize_editor_line_numbers(value)
90
+ EditorMode.normalize_line_numbers(value)
91
+ end
92
+
93
+ def tab_action_result?(result)
94
+ result.is_a?(Hash) && result[:tab_action]
95
+ end
96
+
97
+ def reasoning_action_result?(result)
98
+ result.is_a?(Hash) && result[:reasoning_action]
99
+ end
100
+
101
+ def prompt_action_result?(result)
102
+ tab_action_result?(result) || reasoning_action_result?(result)
103
+ end
104
+
62
105
  end
63
106
  end
64
107
  end
@@ -56,6 +56,8 @@ module Kward
56
56
  def render_cursor_visibility_locked
57
57
  visible = !(@question_state && !selected_question_choice&.fetch(:custom, false))
58
58
  visible = select_editing_active? if @select_state
59
+ visible = git_composing? if @git_state
60
+ visible = project_browser_search_active? if project_browser_visible?
59
61
  set_cursor_visible_locked(visible)
60
62
  end
61
63
 
@@ -66,6 +68,20 @@ module Kward
66
68
  @cursor_visible = visible
67
69
  end
68
70
 
71
+ def set_editor_bar_cursor_locked
72
+ return if @editor_bar_cursor_active
73
+
74
+ @output_io.print(CURSOR_SHAPE_BAR)
75
+ @editor_bar_cursor_active = true
76
+ end
77
+
78
+ def restore_editor_cursor_shape_locked
79
+ return unless @editor_bar_cursor_active
80
+
81
+ @output_io.print(CURSOR_SHAPE_DEFAULT)
82
+ @editor_bar_cursor_active = false
83
+ end
84
+
69
85
  def reserve_composer_region_locked(width: screen_width, height: screen_height)
70
86
  rows, = composer_layout(width, height)
71
87
  ensure_scroll_region_locked(rows.length, width: width, height: height)
@@ -79,7 +95,7 @@ module Kward
79
95
  old_top = [height - old_reserved_rows + 1, 1].max
80
96
  @reserved_rows = new_reserved_rows
81
97
  new_top = composer_top_row(height)
82
- @output_io.print("\e[1;#{transcript_bottom_row(height)}r")
98
+ @output_io.print(TerminalSequences.scroll_region(1, transcript_bottom_row(height)))
83
99
  clear_screen_rows_locked(old_top, new_top - 1) if new_top > old_top
84
100
  @last_composer_rows = []
85
101
  redraw_transcript_locked(width: width, height: height) if redraw_transcript && new_reserved_rows < old_reserved_rows
@@ -103,7 +119,7 @@ module Kward
103
119
  end
104
120
 
105
121
  def restore_scroll_region_locked
106
- @output_io.print("\e[r")
122
+ @output_io.print(TerminalSequences.restore_scroll_region)
107
123
  @reserved_rows = 0
108
124
  end
109
125
 
@@ -172,7 +188,7 @@ module Kward
172
188
  end
173
189
 
174
190
  def move_to_screen(row, col)
175
- @output_io.print("\e[#{row};#{col}H")
191
+ @output_io.print(TerminalSequences.move_to(row, col))
176
192
  end
177
193
 
178
194
  def screen_size
@@ -10,13 +10,7 @@ module Kward
10
10
  return select_current_choice if key.nil?
11
11
  return if handle_select_bracketed_paste_key(key)
12
12
 
13
- if key.is_a?(String) && key.length > 1
14
- token = next_key_token(key)
15
- if token.length < key.length
16
- queue_pending_keys(key[token.length..])
17
- return handle_select_key(token)
18
- end
19
- end
13
+ return true if handle_bundled_key(key) { |token| handle_select_key(token) }
20
14
 
21
15
  return handle_select_confirmation_key(key) if select_confirmation_active?
22
16
 
@@ -101,7 +95,7 @@ module Kward
101
95
  modified_result = handle_select_modified_csi_u_key(code, modifier)
102
96
  return modified_result unless modified_result == false
103
97
 
104
- handle_select_printable_csi_u_key(code, modifiers)
98
+ handle_select_printable_csi_u_key(sequence)
105
99
  end
106
100
  end
107
101
 
@@ -119,12 +113,12 @@ module Kward
119
113
  end
120
114
  end
121
115
 
122
- def handle_select_printable_csi_u_key(code, modifiers)
123
- return false unless modifiers.empty? || modifiers == "1"
124
- return false unless code.between?(32, 126)
116
+ def handle_select_printable_csi_u_key(sequence)
117
+ text = csi_u_printable_text(sequence)
118
+ return true if text.nil? && csi_u_text_field?(sequence)
119
+ return false unless text
125
120
 
126
- key = code.chr(Encoding::UTF_8)
127
- select_typed_key(key)
121
+ select_typed_key(text)
128
122
  end
129
123
 
130
124
  def handle_select_escape_sequence
@@ -153,12 +147,9 @@ module Kward
153
147
  end
154
148
 
155
149
  def handle_select_bracketed_paste_key(key)
156
- paste = read_bracketed_paste(key)
157
- return false unless paste
158
-
159
- select_insert_string(normalize_paste(paste[:content])) if select_editing_active?
160
- queue_pending_keys(paste[:remaining]) if paste[:remaining] && !paste[:remaining].empty?
161
- true
150
+ handle_bracketed_paste(key) do |content|
151
+ select_insert_string(content) if select_editing_active?
152
+ end
162
153
  end
163
154
 
164
155
  def select_current_choice
@@ -37,6 +37,8 @@ module Kward
37
37
  end
38
38
 
39
39
  def slash_overlay_visible?
40
+ return false if @slash_overlay_disabled
41
+
40
42
  composer_input.match?(%r{\A/[^\s/]*\z}) && @slash_overlay_dismissed_input != composer_input && !slash_overlay_matches.empty?
41
43
  end
42
44
 
@@ -10,6 +10,13 @@ module Kward
10
10
  reset
11
11
  end
12
12
 
13
+ def initialize_copy(source)
14
+ super
15
+ @block = source.block&.dup
16
+ @col = source.col
17
+ @pending_wrap = source.pending_wrap?
18
+ end
19
+
13
20
  def reset
14
21
  @block = nil
15
22
  @col = 0
@@ -13,6 +13,12 @@ module Kward
13
13
  @display_rows_cache = nil
14
14
  end
15
15
 
16
+ def initialize_copy(source)
17
+ super
18
+ @text = source.text.dup
19
+ invalidate_display_rows_cache
20
+ end
21
+
16
22
  def to_s
17
23
  @text
18
24
  end