kward 0.72.0 → 0.73.1

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 (71) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +30 -0
  3. data/CHANGELOG.md +59 -0
  4. data/Gemfile.lock +2 -2
  5. data/doc/configuration.md +1 -1
  6. data/doc/editor.md +23 -2
  7. data/doc/git.md +1 -0
  8. data/doc/rpc.md +2 -2
  9. data/doc/shell.md +56 -10
  10. data/doc/usage.md +27 -1
  11. data/lib/kward/ansi.rb +62 -23
  12. data/lib/kward/cli/plugins.rb +1 -1
  13. data/lib/kward/cli/rendering.rb +4 -1
  14. data/lib/kward/cli/runtime_helpers.rb +141 -7
  15. data/lib/kward/cli/settings.rb +0 -1
  16. data/lib/kward/cli/slash_commands.rb +213 -0
  17. data/lib/kward/cli/tabs.rb +34 -4
  18. data/lib/kward/cli/tool_summaries.rb +6 -0
  19. data/lib/kward/cli.rb +4 -12
  20. data/lib/kward/clipboard.rb +2 -3
  21. data/lib/kward/compactor.rb +7 -19
  22. data/lib/kward/config_files.rb +26 -4
  23. data/lib/kward/ekwsh.rb +239 -42
  24. data/lib/kward/image_attachments.rb +3 -1
  25. data/lib/kward/interactive_pty_runner.rb +151 -0
  26. data/lib/kward/local_command_runner.rb +155 -0
  27. data/lib/kward/local_pty_command_runner.rb +171 -0
  28. data/lib/kward/model/context_usage.rb +2 -2
  29. data/lib/kward/model/payloads.rb +2 -5
  30. data/lib/kward/prompt_history.rb +5 -3
  31. data/lib/kward/prompt_interface/editor/auto_indent.rb +5 -4
  32. data/lib/kward/prompt_interface/editor/controller.rb +262 -62
  33. data/lib/kward/prompt_interface/editor/modes/emacs.rb +21 -21
  34. data/lib/kward/prompt_interface/editor/modes/modern.rb +38 -37
  35. data/lib/kward/prompt_interface/editor/modes/vibe.rb +23 -173
  36. data/lib/kward/prompt_interface/editor/modes/vibe_insert_readline.rb +166 -0
  37. data/lib/kward/prompt_interface/editor/renderer.rb +6 -5
  38. data/lib/kward/prompt_interface/editor/state.rb +28 -6
  39. data/lib/kward/prompt_interface/editor/syntax_highlighter.rb +5 -3
  40. data/lib/kward/prompt_interface/git_prompt.rb +12 -23
  41. data/lib/kward/prompt_interface/interactive/controller.rb +1 -1
  42. data/lib/kward/prompt_interface/key_handler.rb +93 -51
  43. data/lib/kward/prompt_interface/question_prompt.rb +1 -6
  44. data/lib/kward/prompt_interface/screen.rb +3 -3
  45. data/lib/kward/prompt_interface/selection_prompt.rb +12 -6
  46. data/lib/kward/prompt_interface/slash_overlay.rb +2 -0
  47. data/lib/kward/prompt_interface.rb +87 -221
  48. data/lib/kward/prompts/commands.rb +4 -0
  49. data/lib/kward/rpc/memory_methods.rb +83 -0
  50. data/lib/kward/rpc/server.rb +130 -83
  51. data/lib/kward/rpc/session_manager.rb +10 -74
  52. data/lib/kward/rpc/tool_metadata.rb +11 -0
  53. data/lib/kward/rpc/transcript_normalizer.rb +4 -39
  54. data/lib/kward/scratchpad_runner.rb +56 -0
  55. data/lib/kward/session_diff.rb +20 -3
  56. data/lib/kward/session_naming.rb +11 -0
  57. data/lib/kward/terminal_keys.rb +84 -0
  58. data/lib/kward/terminal_sequences.rb +42 -0
  59. data/lib/kward/tools/context_for_task.rb +2 -0
  60. data/lib/kward/version.rb +1 -1
  61. data/lib/kward/workers/git_guard.rb +25 -0
  62. data/lib/kward/workers/job.rb +99 -0
  63. data/lib/kward/workers/queue_runner.rb +166 -0
  64. data/lib/kward/workers/queue_store.rb +112 -0
  65. data/lib/kward/workers.rb +3 -0
  66. data/lib/kward/workspace.rb +15 -63
  67. data/templates/default/fulldoc/html/css/kward.css +33 -0
  68. data/templates/default/fulldoc/html/images/kward_screen_1.png +0 -0
  69. data/templates/default/fulldoc/html/setup.rb +1 -0
  70. data/templates/default/layout/html/layout.erb +19 -32
  71. metadata +15 -1
@@ -16,16 +16,19 @@ module Kward
16
16
  class PromptInterface
17
17
  # Mutable state for the built-in composer file editor.
18
18
  class EditorState
19
- attr_reader :path, :original_content, :original_digest, :original_mtime, :original_size
19
+ attr_reader :path, :display_path, :language, :original_content, :original_digest, :original_mtime, :original_size
20
20
  attr_reader :buffer, :undo_stack, :redo_stack, :kill_buffer, :kill_ring, :last_yank_range, :last_yank_index
21
21
  attr_accessor :viewport_row, :viewport_column, :status, :overwrite_confirmed, :quit_confirmed, :search_active, :search_query, :search_direction, :new_file, :editor_mode, :emacs_pending, :readonly, :diff_view
22
22
 
23
- def initialize(path:, content:, new_file: false, editor_mode: "modern", readonly: false, diff_view: false)
24
- @path = path.to_s
23
+ def initialize(path:, content:, new_file: false, editor_mode: "modern", readonly: false, diff_view: false, virtual: false, display_path: nil, language: nil)
24
+ @path = virtual ? nil : path.to_s
25
+ @display_path = display_path.to_s.empty? ? path.to_s : display_path.to_s
26
+ @language = language&.to_sym
25
27
  @new_file = new_file
26
28
  @readonly = readonly
27
29
  @diff_view = diff_view
28
- @file_marker = EditorFileMarker.new(path: @path, content: content, new_file: new_file)
30
+ @virtual = virtual == true
31
+ @file_marker = EditorFileMarker.new(path: @path || @display_path, content: content, new_file: new_file || virtual?)
29
32
  @original_content = @file_marker.content
30
33
  @original_digest = @file_marker.digest
31
34
  @original_mtime = @file_marker.mtime
@@ -61,9 +64,12 @@ module Kward
61
64
 
62
65
  def initialize_copy(other)
63
66
  super
64
- @path = other.path.dup
67
+ @path = other.path&.dup
68
+ @display_path = other.display_path.dup
69
+ @language = other.language
70
+ @virtual = other.virtual?
65
71
  @original_content = other.original_content.dup
66
- @file_marker = EditorFileMarker.new(path: @path, content: @original_content, new_file: other.new_file)
72
+ @file_marker = EditorFileMarker.new(path: @path || @display_path, content: @original_content, new_file: other.new_file || @virtual)
67
73
  @original_digest = other.original_digest.dup
68
74
  @original_mtime = other.original_mtime
69
75
  @original_size = other.original_size
@@ -306,6 +312,18 @@ module Kward
306
312
  @readonly == true
307
313
  end
308
314
 
315
+ def virtual?
316
+ @virtual == true
317
+ end
318
+
319
+ def bind_path(path)
320
+ @path = path.to_s
321
+ @display_path = @path
322
+ @virtual = false
323
+ @new_file = !File.exist?(@path)
324
+ @file_marker = EditorFileMarker.new(path: @path, content: @original_content, new_file: true)
325
+ end
326
+
309
327
  def diff_view?
310
328
  @diff_view == true
311
329
  end
@@ -938,6 +956,8 @@ module Kward
938
956
 
939
957
  def refresh_after_save(content)
940
958
  @new_file = false
959
+ @virtual = false
960
+ @display_path = @path.to_s
941
961
  @file_marker.refresh(content)
942
962
  @original_content = @file_marker.content
943
963
  @original_digest = @file_marker.digest
@@ -949,6 +969,8 @@ module Kward
949
969
  end
950
970
 
951
971
  def file_changed_on_disk?
972
+ return false if virtual?
973
+
952
974
  @file_marker.changed_on_disk?(new_file: new_file)
953
975
  end
954
976
 
@@ -134,11 +134,13 @@ module Kward
134
134
 
135
135
  def editor_syntax_language
136
136
  return nil unless @editor_state
137
+ return @editor_state.language if @editor_state.language
137
138
 
139
+ path = @editor_state.path || @editor_state.display_path
138
140
  @editor_syntax_language_path ||= nil
139
- if @editor_syntax_language_path != @editor_state.path
140
- @editor_syntax_language_path = @editor_state.path
141
- @editor_syntax_language = editor_detect_syntax_language(@editor_state.path)
141
+ if @editor_syntax_language_path != path
142
+ @editor_syntax_language_path = path
143
+ @editor_syntax_language = editor_detect_syntax_language(path)
142
144
  end
143
145
  @editor_syntax_language
144
146
  end
@@ -45,6 +45,14 @@ module Kward
45
45
  end
46
46
  end
47
47
 
48
+ def open_modal_diff_viewer(path, content)
49
+ @mutex.synchronize do
50
+ open_diff_viewer(path.to_s, content.to_s)
51
+ render_prompt_locked
52
+ end
53
+ read_editor_until_closed
54
+ end
55
+
48
56
  private
49
57
 
50
58
  def handle_git_key(key)
@@ -116,12 +124,9 @@ module Kward
116
124
  end
117
125
 
118
126
  def handle_git_bracketed_paste_key(key)
119
- paste = read_bracketed_paste(key)
120
- return false unless paste
121
-
122
- insert_string(normalize_paste(paste[:content])) if git_composing?
123
- queue_pending_keys(paste[:remaining]) if paste[:remaining] && !paste[:remaining].empty?
124
- true
127
+ handle_bracketed_paste(key) do |content|
128
+ insert_string(content) if git_composing?
129
+ end
125
130
  end
126
131
 
127
132
  def handle_git_named_key(key_name)
@@ -149,18 +154,6 @@ module Kward
149
154
  end
150
155
  end
151
156
 
152
- def handle_git_escape_sequence
153
- pending_sequence = read_pending_escape_sequence
154
- return SELECT_CANCEL if pending_sequence.empty?
155
-
156
- full_sequence = "\e#{pending_sequence}"
157
- sequence = next_key_token(full_sequence)
158
- queue_pending_keys(full_sequence[sequence.length..]) if full_sequence.length > sequence.length
159
- return SELECT_CANCEL if sequence == "\e"
160
-
161
- handle_git_named_key(key_name_for(sequence))
162
- end
163
-
164
157
  def git_state_for(status_lines, selected_index: 0)
165
158
  lines = Array(status_lines).map(&:to_s)
166
159
  selected_index = [[selected_index.to_i, 0].max, [lines.length - 1, 0].max].min
@@ -204,11 +197,7 @@ module Kward
204
197
  def open_git_diff_viewer(diff)
205
198
  return unless diff.respond_to?(:[])
206
199
 
207
- @mutex.synchronize do
208
- open_diff_viewer(diff[:path].to_s, diff[:content].to_s)
209
- render_prompt_locked
210
- end
211
- read_editor_until_closed
200
+ open_modal_diff_viewer(diff[:path], diff[:content])
212
201
  end
213
202
 
214
203
  def read_editor_until_closed
@@ -50,7 +50,7 @@ module Kward
50
50
  # @param row [Integer] zero-based row
51
51
  # @param col [Integer] zero-based column
52
52
  # @param char [String] single character to display
53
- # @param color [Symbol, String, nil] ANSI style name or raw SGR code
53
+ # @param colors [Array<Symbol, String>] ANSI style names or raw SGR codes
54
54
  # @return [void]
55
55
  def put(row, col, char, *colors)
56
56
  row = row.to_i
@@ -14,7 +14,7 @@ module Kward
14
14
 
15
15
  @reader.read_keypress(echo: false, raw: true, nonblock: nonblock)
16
16
  rescue TTY::Reader::InputInterrupt
17
- "\x03"
17
+ TerminalKeys::CTRL_C
18
18
  rescue IO::WaitReadable, Errno::EAGAIN, Errno::EWOULDBLOCK
19
19
  nil
20
20
  end
@@ -116,11 +116,11 @@ module Kward
116
116
  handle_tab_completion_key
117
117
  when "\b", "\x7F"
118
118
  delete_before_cursor
119
- when "\x04"
119
+ when TerminalKeys::CTRL_D
120
120
  delete_at_cursor_or_exit
121
- when "\x03"
121
+ when TerminalKeys::CTRL_C
122
122
  cancel_input_or_interrupt
123
- when "\x12"
123
+ when TerminalKeys::CTRL_R
124
124
  start_history_search
125
125
  when "\e"
126
126
  handle_escape_sequence
@@ -152,7 +152,7 @@ module Kward
152
152
  accept_history_search
153
153
  when "\b", "\x7F"
154
154
  update_history_search_query(composer_input[0...-1].to_s)
155
- when "\x03", "\e"
155
+ when TerminalKeys::CTRL_C, "\e"
156
156
  cancel_history_search
157
157
  else
158
158
  append_history_search_key(key)
@@ -193,7 +193,7 @@ module Kward
193
193
  def cancel_input_or_interrupt
194
194
  return CANCEL_INPUT if @busy
195
195
 
196
- raise Interrupt
196
+ true
197
197
  end
198
198
 
199
199
  def handle_tab_completion_key
@@ -253,21 +253,41 @@ module Kward
253
253
  end
254
254
 
255
255
  def handle_bracketed_paste_key(key)
256
+ handle_bracketed_paste(key) { |content| insert_paste(content) }
257
+ end
258
+
259
+ def handle_mouse_reporting_key(key)
260
+ event = parse_sgr_mouse_event(key)
261
+ return false unless event
262
+
263
+ queue_pending_keys(event[:remaining]) unless event[:remaining].empty?
264
+ true
265
+ end
266
+
267
+ def handle_bracketed_paste(key)
256
268
  paste = read_bracketed_paste(key)
257
269
  return false unless paste
258
270
 
259
- insert_paste(normalize_paste(paste[:content]))
271
+ yield normalize_paste(paste[:content])
260
272
  queue_pending_keys(paste[:remaining]) if paste[:remaining] && !paste[:remaining].empty?
261
273
  true
262
274
  end
263
275
 
264
- def handle_mouse_reporting_key(key)
265
- text = key.to_s
266
- match = text.match(/\A(?:\e)?\[<\d+;\d+;\d+[Mm]/)
267
- return false unless match
276
+ def parse_sgr_mouse_event(key)
277
+ match = key.to_s.match(/\A(?:\e)?\[<(\d+);(\d+);(\d+)([Mm])/)
278
+ return nil unless match
268
279
 
269
- queue_pending_keys(text[match[0].length..]) if match[0].length < text.length
270
- true
280
+ code = match[1].to_i
281
+ {
282
+ code: code,
283
+ button: code & 3,
284
+ column: match[2].to_i,
285
+ row: match[3].to_i,
286
+ action: match[4],
287
+ release: match[4] == "m",
288
+ drag: (code & 32).positive?,
289
+ remaining: key.to_s[match[0].length..].to_s
290
+ }
271
291
  end
272
292
 
273
293
  def read_bracketed_paste(key)
@@ -329,7 +349,7 @@ module Kward
329
349
  end
330
350
 
331
351
  def parse_csi_u_key(key)
332
- match = key.to_s.match(/\A\e\[(\d+)((?:;[\d:]*)*)u/)
352
+ match = key.to_s.match(TerminalKeys::CSI_U_PATTERN)
333
353
  return nil unless match
334
354
 
335
355
  fields = match[2].to_s.split(";", -1)[1..] || []
@@ -345,12 +365,34 @@ module Kward
345
365
  }
346
366
  end
347
367
 
368
+ def csi_u_key_event(sequence)
369
+ code = sequence[:code]
370
+ case code
371
+ when 9
372
+ { type: :tab, modifier: sequence[:modifier] }
373
+ when 13
374
+ { type: :enter, modifier: sequence[:modifier] }
375
+ when 27
376
+ { type: :escape, modifier: sequence[:modifier] }
377
+ when 8, 127
378
+ { type: :backspace, modifier: sequence[:modifier] }
379
+ when 4
380
+ { type: :delete, modifier: sequence[:modifier] }
381
+ else
382
+ text = csi_u_printable_text(sequence)
383
+ return { type: :printable, text: text, modifier: sequence[:modifier] } if text
384
+ return { type: :text_field, modifier: sequence[:modifier] } if csi_u_text_field?(sequence)
385
+
386
+ { type: :modified, code: code, modifier: sequence[:modifier] }
387
+ end
388
+ end
389
+
348
390
  def insert_csi_u_text(sequence)
349
- text = csi_u_printable_text(sequence)
350
- return true if text.nil? && csi_u_text_field?(sequence)
351
- return false unless text
391
+ event = csi_u_key_event(sequence)
392
+ return true if event[:type] == :text_field
393
+ return false unless event[:type] == :printable
352
394
 
353
- insert_string(text)
395
+ insert_string(event[:text])
354
396
  end
355
397
 
356
398
  def csi_u_text_field?(sequence)
@@ -444,17 +486,17 @@ module Kward
444
486
  def cursor_key_name(key)
445
487
  text = key.to_s
446
488
  case text
447
- when /\A\e\[[0-9;:]*A\z/, "\eOA"
489
+ when TerminalKeys::UP_PATTERN, *TerminalKeys::UP
448
490
  :up
449
- when /\A\e\[[0-9;:]*B\z/, "\eOB"
491
+ when TerminalKeys::DOWN_PATTERN, *TerminalKeys::DOWN
450
492
  :down
451
- when /\A\e\[[0-9;:]*C\z/, "\eOC"
493
+ when TerminalKeys::RIGHT_PATTERN, *TerminalKeys::RIGHT
452
494
  :right
453
- when /\A\e\[[0-9;:]*D\z/, "\eOD"
495
+ when TerminalKeys::LEFT_PATTERN, *TerminalKeys::LEFT
454
496
  :left
455
- when "\e[5~"
497
+ when *TerminalKeys::PAGE_UP
456
498
  :pageup
457
- when "\e[6~"
499
+ when *TerminalKeys::PAGE_DOWN
458
500
  :pagedown
459
501
  end
460
502
  end
@@ -506,8 +548,8 @@ module Kward
506
548
 
507
549
  def next_key_token(keys)
508
550
  text = keys.to_s
509
- text.match(/\A\e\[[0-9;:]*[A-Za-z~]/)&.[](0) ||
510
- text.match(/\A\eO[A-Za-z]/)&.[](0) ||
551
+ text.match(TerminalKeys::CSI_KEY_PATTERN)&.[](0) ||
552
+ text.match(TerminalKeys::SS3_KEY_PATTERN)&.[](0) ||
511
553
  shift_enter_sequence_for(text) ||
512
554
  (text.start_with?("\e") && text.length > 1 && alt_key_sequence?(text[1]) ? text[0, 2] : text[0, 1])
513
555
  end
@@ -536,9 +578,9 @@ module Kward
536
578
  sequence
537
579
  end
538
580
 
539
- CTRL_TAB_SEQUENCES = ["\e[9;5u", "\e[27;5;9~", "\e[1;5I"].freeze
540
- CTRL_SHIFT_TAB_SEQUENCES = ["\e[9;6u", "\e[27;6;9~", "\e[1;6I", "\e[1;6Z"].freeze
541
- SHIFT_TAB_SEQUENCES = ["\e[Z", "\e[1;2Z", "\e[9;2u", "\e[27;2;9~", "\e[1;2I"].freeze
581
+ CTRL_TAB_SEQUENCES = TerminalKeys::CTRL_TAB
582
+ CTRL_SHIFT_TAB_SEQUENCES = TerminalKeys::CTRL_SHIFT_TAB
583
+ SHIFT_TAB_SEQUENCES = TerminalKeys::SHIFT_TAB
542
584
 
543
585
  def handle_completion_provider_key(key)
544
586
  return false unless key == "\t" && @completion_provider
@@ -617,9 +659,9 @@ module Kward
617
659
 
618
660
  def handle_ctrl_tab_key_binding(key)
619
661
  case key
620
- when "\x14", "\e[116;5u"
662
+ when TerminalKeys::CTRL_T, TerminalKeys::CTRL_T_CSI_U
621
663
  { tab_action: :new }
622
- when "\e[119;5u"
664
+ when TerminalKeys::CTRL_W_CSI_U
623
665
  { tab_action: :close }
624
666
  else
625
667
  ctrl_number_tab_action(key)
@@ -627,7 +669,7 @@ module Kward
627
669
  end
628
670
 
629
671
  def ctrl_number_tab_action(key)
630
- match = key.to_s.match(/\A\e\[((?:49)|(?:5[0-7]));5u\z/)
672
+ match = key.to_s.match(TerminalKeys::CTRL_NUMBER_TAB_PATTERN)
631
673
  return false unless match
632
674
 
633
675
  { tab_action: :select, index: match[1].to_i - 49 }
@@ -637,9 +679,9 @@ module Kward
637
679
  case key
638
680
  when "\et", "\eT"
639
681
  { tab_action: :new }
640
- when "\e[1;3C", "\e[3C"
682
+ when *TerminalKeys::ALT_RIGHT
641
683
  { tab_action: :next }
642
- when "\e[1;3D", "\e[3D"
684
+ when *TerminalKeys::ALT_LEFT
643
685
  { tab_action: :previous }
644
686
  else
645
687
  alt_number_tab_action(key)
@@ -655,35 +697,35 @@ module Kward
655
697
 
656
698
  def handle_composer_key_binding(key)
657
699
  case key
658
- when "\x01"
700
+ when TerminalKeys::CTRL_A
659
701
  move_to_start_of_line
660
- when "\x02"
702
+ when TerminalKeys::CTRL_B
661
703
  move_cursor_left
662
- when "\x04"
704
+ when TerminalKeys::CTRL_D
663
705
  delete_at_cursor_or_exit
664
- when "\x05"
706
+ when TerminalKeys::CTRL_E
665
707
  move_to_end_of_line
666
- when "\x06"
708
+ when TerminalKeys::CTRL_F
667
709
  move_cursor_right
668
- when "\x0B"
710
+ when TerminalKeys::CTRL_K
669
711
  kill_line_after_cursor
670
- when "\x0C"
712
+ when TerminalKeys::CTRL_L
671
713
  redraw_screen_locked
672
- when "\x15"
714
+ when TerminalKeys::CTRL_U
673
715
  kill_line_before_cursor
674
- when "\x17"
716
+ when TerminalKeys::CTRL_W
675
717
  delete_word_before_cursor
676
- when "\x19"
718
+ when TerminalKeys::CTRL_Y
677
719
  yank_kill_buffer
678
- when "\e[D", "\eOD"
720
+ when *TerminalKeys::LEFT
679
721
  move_cursor_left
680
- when "\e[C", "\eOC"
722
+ when *TerminalKeys::RIGHT
681
723
  move_cursor_right
682
- when "\e[H", "\eOH", "\e[1~", "\e[7~"
724
+ when *TerminalKeys::HOME
683
725
  move_to_start_of_line
684
- when "\e[F", "\eOF", "\e[4~", "\e[8~"
726
+ when *TerminalKeys::END_KEY
685
727
  move_to_end_of_line
686
- when "\e[3~"
728
+ when *TerminalKeys::DELETE
687
729
  delete_at_cursor
688
730
  when "\eb", "\eB"
689
731
  move_to_previous_word
@@ -699,9 +741,9 @@ module Kward
699
741
  end
700
742
 
701
743
  def parse_modified_ansi_key(key)
702
- if (match = key.to_s.match(/\A\e\[(\d+);(\d+)([CDFH])\z/))
744
+ if (match = key.to_s.match(TerminalKeys::MODIFIED_CURSOR_PATTERN))
703
745
  { type: :cursor, modifier: match[2].to_i, final: match[3] }
704
- elsif (match = key.to_s.match(/\A\e\[3;(\d+)~\z/))
746
+ elsif (match = key.to_s.match(TerminalKeys::MODIFIED_DELETE_PATTERN))
705
747
  { type: :delete, modifier: match[1].to_i }
706
748
  end
707
749
  end
@@ -195,12 +195,7 @@ module Kward
195
195
  end
196
196
 
197
197
  def handle_question_bracketed_paste_key(key)
198
- paste = read_bracketed_paste(key)
199
- return false unless paste
200
-
201
- question_insert_string(normalize_paste(paste[:content]))
202
- queue_pending_keys(paste[:remaining]) if paste[:remaining] && !paste[:remaining].empty?
203
- true
198
+ handle_bracketed_paste(key) { |content| question_insert_string(content) }
204
199
  end
205
200
 
206
201
  def current_question_answer
@@ -95,7 +95,7 @@ module Kward
95
95
  old_top = [height - old_reserved_rows + 1, 1].max
96
96
  @reserved_rows = new_reserved_rows
97
97
  new_top = composer_top_row(height)
98
- @output_io.print("\e[1;#{transcript_bottom_row(height)}r")
98
+ @output_io.print(TerminalSequences.scroll_region(1, transcript_bottom_row(height)))
99
99
  clear_screen_rows_locked(old_top, new_top - 1) if new_top > old_top
100
100
  @last_composer_rows = []
101
101
  redraw_transcript_locked(width: width, height: height) if redraw_transcript && new_reserved_rows < old_reserved_rows
@@ -119,7 +119,7 @@ module Kward
119
119
  end
120
120
 
121
121
  def restore_scroll_region_locked
122
- @output_io.print("\e[r")
122
+ @output_io.print(TerminalSequences.restore_scroll_region)
123
123
  @reserved_rows = 0
124
124
  end
125
125
 
@@ -188,7 +188,7 @@ module Kward
188
188
  end
189
189
 
190
190
  def move_to_screen(row, col)
191
- @output_io.print("\e[#{row};#{col}H")
191
+ @output_io.print(TerminalSequences.move_to(row, col))
192
192
  end
193
193
 
194
194
  def screen_size
@@ -147,12 +147,9 @@ module Kward
147
147
  end
148
148
 
149
149
  def handle_select_bracketed_paste_key(key)
150
- paste = read_bracketed_paste(key)
151
- return false unless paste
152
-
153
- select_insert_string(normalize_paste(paste[:content])) if select_editing_active?
154
- queue_pending_keys(paste[:remaining]) if paste[:remaining] && !paste[:remaining].empty?
155
- true
150
+ handle_bracketed_paste(key) do |content|
151
+ select_insert_string(content) if select_editing_active?
152
+ end
156
153
  end
157
154
 
158
155
  def select_current_choice
@@ -160,6 +157,15 @@ module Kward
160
157
  end
161
158
 
162
159
  def handle_select_confirmation_key(key)
160
+ sequence = parse_csi_u_key(key)
161
+ if sequence
162
+ queue_pending_keys(sequence[:remaining]) if sequence[:remaining] && !sequence[:remaining].empty?
163
+ text = csi_u_printable_text(sequence)
164
+ return true if text.nil? && csi_u_text_field?(sequence)
165
+
166
+ key = text if text
167
+ end
168
+
163
169
  if key.to_s.start_with?("\e")
164
170
  clear_select_confirmation
165
171
  return true
@@ -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