kward 0.66.0 → 0.67.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +44 -3
- data/Gemfile.lock +2 -2
- data/README.md +5 -1
- data/doc/configuration.md +43 -1
- data/doc/memory.md +31 -9
- data/doc/rpc.md +41 -21
- data/doc/troubleshooting.md +55 -0
- data/doc/usage.md +41 -6
- data/lib/kward/cli.rb +1155 -195
- data/lib/kward/cli_transcript_formatter.rb +124 -0
- data/lib/kward/compaction/file_operation_tracker.rb +46 -0
- data/lib/kward/compactor.rb +3 -68
- data/lib/kward/config_files.rb +45 -69
- data/lib/kward/memory/manager.rb +66 -7
- data/lib/kward/model/client.rb +2 -195
- data/lib/kward/model/model_info.rb +9 -10
- data/lib/kward/model/payloads.rb +203 -0
- data/lib/kward/prompt_interface/banner.rb +77 -0
- data/lib/kward/prompt_interface.rb +220 -191
- data/lib/kward/prompts/commands.rb +3 -2
- data/lib/kward/rpc/runtime_payloads.rb +79 -0
- data/lib/kward/rpc/server.rb +33 -34
- data/lib/kward/rpc/session_manager.rb +518 -159
- data/lib/kward/rpc/tool_event_normalizer.rb +12 -9
- data/lib/kward/rpc/transcript_normalizer.rb +31 -53
- data/lib/kward/session_store.rb +262 -20
- data/lib/kward/session_trash.rb +96 -0
- data/lib/kward/session_tree_renderer.rb +264 -0
- data/lib/kward/tools/registry.rb +3 -1
- data/lib/kward/version.rb +1 -1
- data/lib/kward/workspace.rb +10 -5
- metadata +9 -1
|
@@ -4,8 +4,7 @@ require "tty-cursor"
|
|
|
4
4
|
require "tty-reader"
|
|
5
5
|
require "tty-screen"
|
|
6
6
|
require_relative "ansi"
|
|
7
|
-
require_relative "
|
|
8
|
-
require_relative "resources/avatar_kward_logo"
|
|
7
|
+
require_relative "prompt_interface/banner"
|
|
9
8
|
|
|
10
9
|
module Kward
|
|
11
10
|
class PromptInterface
|
|
@@ -16,11 +15,8 @@ module Kward
|
|
|
16
15
|
FOOTER_REFRESH_INTERVAL = 1.0
|
|
17
16
|
COMPOSER_MAX_INPUT_ROWS = 6
|
|
18
17
|
TRANSCRIPT_BUFFER_LIMIT = 200_000
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
BANNER_MIN_LOGO_HEIGHT = 4
|
|
22
|
-
BANNER_LOGO_PIXELS = Kward::Resources::AvatarKwardLogo::PIXELS
|
|
23
|
-
BANNER_MESSAGE = "State your business.".freeze
|
|
18
|
+
BANNER_LOGO_PIXELS = Banner::LOGO_PIXELS
|
|
19
|
+
BANNER_MESSAGE = Banner::MESSAGE
|
|
24
20
|
KEYBOARD_PROTOCOL_ENABLE = "\e[>1u".freeze
|
|
25
21
|
KEYBOARD_PROTOCOL_RESTORE = "\e[<u".freeze
|
|
26
22
|
BRACKETED_PASTE_ENABLE = "\e[?2004h".freeze
|
|
@@ -65,10 +61,13 @@ module Kward
|
|
|
65
61
|
@assistant_label = "Assistant"
|
|
66
62
|
@stream_block = nil
|
|
67
63
|
@rendered_rows = 0
|
|
64
|
+
@last_composer_rows = []
|
|
68
65
|
@cursor_rendered_row = 0
|
|
69
66
|
@stream_col = 0
|
|
70
67
|
@stream_pending_wrap = false
|
|
71
68
|
@transcript_buffer = +""
|
|
69
|
+
@transcript_display_rows_cache_width = nil
|
|
70
|
+
@transcript_display_rows_cache = nil
|
|
72
71
|
@visual_banner_count = 0
|
|
73
72
|
@transcript_viewport_rows = 0
|
|
74
73
|
@restoring_transcript = false
|
|
@@ -80,6 +79,7 @@ module Kward
|
|
|
80
79
|
@history = []
|
|
81
80
|
@history_index = nil
|
|
82
81
|
@history_draft = nil
|
|
82
|
+
@prefill_input = nil
|
|
83
83
|
@slash_commands = normalize_slash_commands(slash_commands)
|
|
84
84
|
@slash_selection_index = 0
|
|
85
85
|
@slash_overlay_dismissed_input = nil
|
|
@@ -90,15 +90,14 @@ module Kward
|
|
|
90
90
|
@reserved_rows = 0
|
|
91
91
|
@color_enabled = ANSI.enabled?(output)
|
|
92
92
|
@cursor_visible = true
|
|
93
|
+
@synchronized_output_depth = 0
|
|
93
94
|
@overlay_settings = normalize_overlay_settings(overlay_settings)
|
|
94
95
|
@footer = footer
|
|
95
96
|
@composer_status = composer_status
|
|
96
97
|
@busy_help = busy_help
|
|
97
98
|
@attachment_badges = attachment_badges
|
|
98
99
|
@attachment_parser = attachment_parser
|
|
99
|
-
@
|
|
100
|
-
@banner_logo_pixels = banner_pixels
|
|
101
|
-
@banner_logo_cache = {}
|
|
100
|
+
@banner = Banner.new(message: banner_message, pixels: banner_pixels, screen_height: method(:screen_height))
|
|
102
101
|
end
|
|
103
102
|
|
|
104
103
|
def start
|
|
@@ -140,11 +139,13 @@ module Kward
|
|
|
140
139
|
next
|
|
141
140
|
end
|
|
142
141
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
142
|
+
with_synchronized_output_locked do
|
|
143
|
+
clear_prompt_for_output_locked
|
|
144
|
+
write_transcript_text_locked(text)
|
|
145
|
+
write_transcript_text_locked("\n") unless text.end_with?("\n")
|
|
146
|
+
@stream_block = nil
|
|
147
|
+
render_prompt_after_output_locked
|
|
148
|
+
end
|
|
148
149
|
@output_io.flush
|
|
149
150
|
end
|
|
150
151
|
end
|
|
@@ -153,12 +154,14 @@ module Kward
|
|
|
153
154
|
@mutex.synchronize do
|
|
154
155
|
return if @restoring_transcript
|
|
155
156
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
157
|
+
with_synchronized_output_locked do
|
|
158
|
+
clear_prompt_for_output_locked
|
|
159
|
+
text = message.to_s
|
|
160
|
+
write_visual_transcript_text_locked(text)
|
|
161
|
+
write_visual_transcript_text_locked("\n") unless text.end_with?("\n")
|
|
162
|
+
@stream_block = nil
|
|
163
|
+
render_prompt_after_output_locked
|
|
164
|
+
end
|
|
162
165
|
@output_io.flush
|
|
163
166
|
end
|
|
164
167
|
end
|
|
@@ -168,6 +171,7 @@ module Kward
|
|
|
168
171
|
clear_prompt_for_output_locked
|
|
169
172
|
@output_io.print(SYNCHRONIZED_OUTPUT_ENABLE)
|
|
170
173
|
@transcript_buffer = +""
|
|
174
|
+
invalidate_transcript_display_rows_cache
|
|
171
175
|
@visual_banner_count = 0
|
|
172
176
|
@transcript_viewport_rows = 0
|
|
173
177
|
@stream_block = nil
|
|
@@ -181,7 +185,8 @@ module Kward
|
|
|
181
185
|
@mutex.synchronize do
|
|
182
186
|
@restoring_transcript = false
|
|
183
187
|
@output_io.print(SYNCHRONIZED_OUTPUT_DISABLE)
|
|
184
|
-
|
|
188
|
+
width, height = screen_size
|
|
189
|
+
redraw_screen_locked(width: width, height: height)
|
|
185
190
|
@output_io.flush
|
|
186
191
|
end
|
|
187
192
|
end
|
|
@@ -193,8 +198,9 @@ module Kward
|
|
|
193
198
|
preserve_input = was_composing && !@busy && !@input.empty?
|
|
194
199
|
@prompt_label = message.to_s
|
|
195
200
|
unless preserve_input
|
|
196
|
-
@input =
|
|
197
|
-
@
|
|
201
|
+
@input = @prefill_input.to_s
|
|
202
|
+
@prefill_input = nil
|
|
203
|
+
@cursor = @input.length
|
|
198
204
|
@attachments.clear
|
|
199
205
|
reset_history_navigation
|
|
200
206
|
end
|
|
@@ -235,7 +241,7 @@ module Kward
|
|
|
235
241
|
answer.start_with?("y")
|
|
236
242
|
end
|
|
237
243
|
|
|
238
|
-
def select(message, choices, title: "Sessions", custom: false)
|
|
244
|
+
def select(message, choices, title: "Sessions", custom: false, initial_index: 0)
|
|
239
245
|
return nil if choices.empty? && !custom
|
|
240
246
|
|
|
241
247
|
start
|
|
@@ -248,7 +254,9 @@ module Kward
|
|
|
248
254
|
@asking = true
|
|
249
255
|
@busy = false
|
|
250
256
|
@queued_count = 0
|
|
251
|
-
|
|
257
|
+
choice_labels = choices.map(&:to_s)
|
|
258
|
+
selection_index = choice_labels.empty? ? 0 : [[initial_index.to_i, 0].max, choice_labels.length - 1].min
|
|
259
|
+
@select_state = { choices: choice_labels, selection_index: selection_index, title: title.to_s, custom: custom }
|
|
252
260
|
reset_history_navigation
|
|
253
261
|
render_prompt_locked
|
|
254
262
|
end
|
|
@@ -390,59 +398,54 @@ module Kward
|
|
|
390
398
|
|
|
391
399
|
def print_visual_banner
|
|
392
400
|
@mutex.synchronize do
|
|
393
|
-
|
|
401
|
+
width, height = screen_size
|
|
402
|
+
rows = banner_rows(width)
|
|
394
403
|
return if rows.empty?
|
|
395
404
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
405
|
+
with_synchronized_output_locked do
|
|
406
|
+
prepare_transcript_output_locked
|
|
407
|
+
rows.each do |row|
|
|
408
|
+
write_visual_transcript_text_locked(row)
|
|
409
|
+
write_visual_transcript_text_locked("\n")
|
|
410
|
+
end
|
|
411
|
+
@visual_banner_count += 1
|
|
412
|
+
invalidate_transcript_display_rows_cache
|
|
413
|
+
remember_transcript_viewport_locked(height)
|
|
414
|
+
@stream_block = nil
|
|
415
|
+
restore_composer_cursor_locked
|
|
400
416
|
end
|
|
401
|
-
@visual_banner_count += 1
|
|
402
|
-
remember_transcript_viewport_locked
|
|
403
|
-
@stream_block = nil
|
|
404
|
-
restore_composer_cursor_locked
|
|
405
417
|
@output_io.flush
|
|
406
418
|
end
|
|
407
419
|
end
|
|
408
420
|
|
|
409
421
|
def start_stream_block(label)
|
|
410
422
|
@mutex.synchronize do
|
|
411
|
-
|
|
412
|
-
prepare_transcript_output_locked unless @restoring_transcript
|
|
413
|
-
ensure_transcript_block_separator_locked
|
|
414
|
-
write_transcript_text_locked("#{colored("#{transcript_label(label)}>", label_color(label), :bold)}\n")
|
|
415
|
-
@stream_block = label
|
|
416
|
-
end
|
|
417
|
-
restore_composer_cursor_locked unless @restoring_transcript
|
|
418
|
-
@output_io.flush unless @restoring_transcript
|
|
423
|
+
write_stream_block_locked(label, "", finish: false)
|
|
419
424
|
end
|
|
420
425
|
end
|
|
421
426
|
|
|
422
427
|
def write_delta(delta)
|
|
423
428
|
@mutex.synchronize do
|
|
424
|
-
|
|
425
|
-
write_transcript_text_locked(delta.to_s)
|
|
426
|
-
restore_composer_cursor_locked unless @restoring_transcript
|
|
427
|
-
@output_io.flush unless @restoring_transcript
|
|
429
|
+
write_stream_block_locked(nil, delta.to_s, finish: false)
|
|
428
430
|
end
|
|
429
431
|
end
|
|
430
432
|
|
|
431
433
|
def finish_stream_block
|
|
432
434
|
@mutex.synchronize do
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
435
|
+
write_stream_block_locked(nil, "", finish: true)
|
|
436
|
+
end
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
def write_stream_block(label, delta, finish: false)
|
|
440
|
+
@mutex.synchronize do
|
|
441
|
+
write_stream_block_locked(label, delta.to_s, finish: finish)
|
|
440
442
|
end
|
|
441
443
|
end
|
|
442
444
|
|
|
443
445
|
def redraw
|
|
444
446
|
@mutex.synchronize do
|
|
445
|
-
|
|
447
|
+
width, height = screen_size
|
|
448
|
+
with_synchronized_output_locked { redraw_screen_locked(width: width, height: height) }
|
|
446
449
|
@output_io.flush
|
|
447
450
|
end
|
|
448
451
|
end
|
|
@@ -450,12 +453,14 @@ module Kward
|
|
|
450
453
|
def clear_transcript
|
|
451
454
|
@mutex.synchronize do
|
|
452
455
|
@transcript_buffer = +""
|
|
456
|
+
invalidate_transcript_display_rows_cache
|
|
453
457
|
@visual_banner_count = 0
|
|
454
458
|
@transcript_viewport_rows = 0
|
|
455
459
|
@stream_block = nil
|
|
456
460
|
@stream_col = 0
|
|
457
461
|
@stream_pending_wrap = false
|
|
458
|
-
|
|
462
|
+
width, height = screen_size
|
|
463
|
+
with_synchronized_output_locked { redraw_screen_locked(width: width, height: height) }
|
|
459
464
|
@output_io.flush
|
|
460
465
|
end
|
|
461
466
|
end
|
|
@@ -486,6 +491,39 @@ module Kward
|
|
|
486
491
|
@raw_mode_active = false
|
|
487
492
|
end
|
|
488
493
|
|
|
494
|
+
def write_stream_block_locked(label, delta, finish: false)
|
|
495
|
+
with_synchronized_output_locked do
|
|
496
|
+
prepare_transcript_output_locked unless @restoring_transcript
|
|
497
|
+
if label && @stream_block != label
|
|
498
|
+
ensure_transcript_block_separator_locked
|
|
499
|
+
write_transcript_text_locked("#{colored("#{transcript_label(label)}>", label_color(label), :bold)}\n")
|
|
500
|
+
@stream_block = label
|
|
501
|
+
end
|
|
502
|
+
write_transcript_text_locked(delta) unless delta.empty?
|
|
503
|
+
write_transcript_text_locked("\n") if finish && @stream_block
|
|
504
|
+
@stream_block = nil if finish
|
|
505
|
+
restore_composer_cursor_locked unless @restoring_transcript
|
|
506
|
+
end
|
|
507
|
+
@output_io.flush unless @restoring_transcript
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
def with_synchronized_output_locked
|
|
511
|
+
if @restoring_transcript || @synchronized_output_depth.positive?
|
|
512
|
+
yield
|
|
513
|
+
return
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
synchronized = true
|
|
517
|
+
@synchronized_output_depth += 1
|
|
518
|
+
@output_io.print(SYNCHRONIZED_OUTPUT_ENABLE)
|
|
519
|
+
yield
|
|
520
|
+
ensure
|
|
521
|
+
if synchronized
|
|
522
|
+
@synchronized_output_depth -= 1
|
|
523
|
+
@output_io.print(SYNCHRONIZED_OUTPUT_DISABLE) if @synchronized_output_depth.zero?
|
|
524
|
+
end
|
|
525
|
+
end
|
|
526
|
+
|
|
489
527
|
def write_transcript_text_locked(text)
|
|
490
528
|
append_transcript_buffer(text.to_s)
|
|
491
529
|
remember_transcript_viewport_locked unless text.to_s.empty?
|
|
@@ -493,19 +531,26 @@ module Kward
|
|
|
493
531
|
end
|
|
494
532
|
|
|
495
533
|
def write_visual_transcript_text_locked(text)
|
|
534
|
+
width, height = screen_size
|
|
496
535
|
output_text = terminal_newlines(text.to_s)
|
|
497
|
-
advance_pending_stream_wrap_locked(output_text)
|
|
536
|
+
advance_pending_stream_wrap_locked(output_text, width: width, height: height)
|
|
498
537
|
@output_io.print(output_text)
|
|
499
|
-
update_stream_position(output_text)
|
|
538
|
+
update_stream_position(output_text, width: width)
|
|
500
539
|
end
|
|
501
540
|
|
|
502
541
|
def append_transcript_buffer(text)
|
|
503
542
|
@transcript_buffer << ANSI.sanitize_transcript(text)
|
|
543
|
+
invalidate_transcript_display_rows_cache
|
|
504
544
|
return if @transcript_buffer.length <= TRANSCRIPT_BUFFER_LIMIT
|
|
505
545
|
|
|
506
546
|
@transcript_buffer = @transcript_buffer[-TRANSCRIPT_BUFFER_LIMIT, TRANSCRIPT_BUFFER_LIMIT]
|
|
507
547
|
end
|
|
508
548
|
|
|
549
|
+
def invalidate_transcript_display_rows_cache
|
|
550
|
+
@transcript_display_rows_cache_width = nil
|
|
551
|
+
@transcript_display_rows_cache = nil
|
|
552
|
+
end
|
|
553
|
+
|
|
509
554
|
def ensure_transcript_block_separator_locked
|
|
510
555
|
return if @transcript_buffer.empty? || @transcript_buffer.end_with?("\n\n")
|
|
511
556
|
|
|
@@ -1618,6 +1663,12 @@ module Kward
|
|
|
1618
1663
|
@cursor = @input.length
|
|
1619
1664
|
end
|
|
1620
1665
|
|
|
1666
|
+
def prefill_input(value)
|
|
1667
|
+
@mutex.synchronize do
|
|
1668
|
+
@prefill_input = value.to_s
|
|
1669
|
+
end
|
|
1670
|
+
end
|
|
1671
|
+
|
|
1621
1672
|
def reset_history_navigation
|
|
1622
1673
|
@history_index = nil
|
|
1623
1674
|
@history_draft = nil
|
|
@@ -1698,14 +1749,15 @@ module Kward
|
|
|
1698
1749
|
return unless @started && @asking
|
|
1699
1750
|
|
|
1700
1751
|
handle_resize_locked
|
|
1701
|
-
|
|
1702
|
-
|
|
1752
|
+
width, height = screen_size
|
|
1753
|
+
rows, cursor_row, cursor_col = composer_layout(width, height)
|
|
1754
|
+
ensure_scroll_region_locked(rows.length, width: width, height: height)
|
|
1703
1755
|
@rendered_rows = rows.length
|
|
1704
|
-
render_composer_rows_locked(rows)
|
|
1756
|
+
render_composer_rows_locked(rows, height: height)
|
|
1705
1757
|
@cursor_rendered_row = cursor_row
|
|
1706
|
-
@last_width =
|
|
1707
|
-
@last_height =
|
|
1708
|
-
move_to_screen(composer_top_row + cursor_row, cursor_col + 1)
|
|
1758
|
+
@last_width = width
|
|
1759
|
+
@last_height = height
|
|
1760
|
+
move_to_screen(composer_top_row(height) + cursor_row, cursor_col + 1)
|
|
1709
1761
|
render_cursor_visibility_locked
|
|
1710
1762
|
@output_io.flush
|
|
1711
1763
|
end
|
|
@@ -1716,26 +1768,29 @@ module Kward
|
|
|
1716
1768
|
|
|
1717
1769
|
def clear_prompt_locked
|
|
1718
1770
|
handle_resize_locked
|
|
1719
|
-
|
|
1771
|
+
width, height = screen_size
|
|
1772
|
+
clear_composer_region_locked(height: height)
|
|
1720
1773
|
@rendered_rows = 0
|
|
1721
1774
|
@cursor_rendered_row = 0
|
|
1722
|
-
redraw_transcript_locked
|
|
1775
|
+
redraw_transcript_locked(width: width, height: height)
|
|
1723
1776
|
end
|
|
1724
1777
|
|
|
1725
1778
|
def clear_prompt_for_output_locked
|
|
1726
1779
|
handle_resize_locked
|
|
1727
|
-
|
|
1728
|
-
|
|
1780
|
+
width, height = screen_size
|
|
1781
|
+
reserve_composer_region_locked(width: width, height: height) if @started && @asking
|
|
1782
|
+
clear_composer_region_locked(height: height)
|
|
1729
1783
|
@rendered_rows = 0
|
|
1730
1784
|
@cursor_rendered_row = 0
|
|
1731
|
-
move_to_transcript_cursor_locked if @started
|
|
1785
|
+
move_to_transcript_cursor_locked(width: width, height: height) if @started
|
|
1732
1786
|
end
|
|
1733
1787
|
|
|
1734
1788
|
def prepare_transcript_output_locked
|
|
1735
1789
|
handle_resize_locked
|
|
1790
|
+
width, height = screen_size
|
|
1736
1791
|
hide_cursor_for_transcript_output_locked
|
|
1737
|
-
reserve_composer_region_locked
|
|
1738
|
-
move_to_transcript_cursor_locked
|
|
1792
|
+
reserve_composer_region_locked(width: width, height: height)
|
|
1793
|
+
move_to_transcript_cursor_locked(width: width, height: height)
|
|
1739
1794
|
end
|
|
1740
1795
|
|
|
1741
1796
|
def hide_cursor_for_transcript_output_locked
|
|
@@ -1747,8 +1802,9 @@ module Kward
|
|
|
1747
1802
|
def restore_composer_cursor_locked
|
|
1748
1803
|
return unless @started && @asking
|
|
1749
1804
|
|
|
1750
|
-
|
|
1751
|
-
|
|
1805
|
+
width, height = screen_size
|
|
1806
|
+
_rows, cursor_row, cursor_col = composer_layout(width, height)
|
|
1807
|
+
move_to_screen(composer_top_row(height) + cursor_row, cursor_col + 1)
|
|
1752
1808
|
render_cursor_visibility_locked
|
|
1753
1809
|
end
|
|
1754
1810
|
|
|
@@ -1764,26 +1820,25 @@ module Kward
|
|
|
1764
1820
|
@cursor_visible = visible
|
|
1765
1821
|
end
|
|
1766
1822
|
|
|
1767
|
-
def reserve_composer_region_locked
|
|
1768
|
-
rows, = composer_layout(
|
|
1769
|
-
ensure_scroll_region_locked(rows.length)
|
|
1823
|
+
def reserve_composer_region_locked(width: screen_width, height: screen_height)
|
|
1824
|
+
rows, = composer_layout(width, height)
|
|
1825
|
+
ensure_scroll_region_locked(rows.length, width: width, height: height)
|
|
1770
1826
|
end
|
|
1771
1827
|
|
|
1772
|
-
def ensure_scroll_region_locked(row_count, redraw_transcript: true)
|
|
1773
|
-
new_reserved_rows = [[row_count, 1].max, [
|
|
1774
|
-
return if @reserved_rows == new_reserved_rows && @last_height ==
|
|
1828
|
+
def ensure_scroll_region_locked(row_count, redraw_transcript: true, width: screen_width, height: screen_height)
|
|
1829
|
+
new_reserved_rows = [[row_count, 1].max, [height - 1, 1].max].min
|
|
1830
|
+
return if @reserved_rows == new_reserved_rows && @last_height == height
|
|
1775
1831
|
|
|
1776
1832
|
old_reserved_rows = @reserved_rows
|
|
1777
1833
|
rows_to_clear = [old_reserved_rows, new_reserved_rows].max
|
|
1778
1834
|
@reserved_rows = new_reserved_rows
|
|
1779
|
-
@output_io.print("\e[1;#{transcript_bottom_row}r")
|
|
1780
|
-
clear_composer_region_locked(rows_to_clear)
|
|
1781
|
-
redraw_transcript_locked if redraw_transcript && new_reserved_rows < old_reserved_rows
|
|
1835
|
+
@output_io.print("\e[1;#{transcript_bottom_row(height)}r")
|
|
1836
|
+
clear_composer_region_locked(rows_to_clear, height: height)
|
|
1837
|
+
redraw_transcript_locked(width: width, height: height) if redraw_transcript && new_reserved_rows < old_reserved_rows
|
|
1782
1838
|
end
|
|
1783
1839
|
|
|
1784
1840
|
def handle_resize_locked
|
|
1785
|
-
current_width =
|
|
1786
|
-
current_height = screen_height
|
|
1841
|
+
current_width, current_height = screen_size
|
|
1787
1842
|
return false if current_width == @last_width && current_height == @last_height
|
|
1788
1843
|
|
|
1789
1844
|
old_width = @last_width
|
|
@@ -1795,7 +1850,7 @@ module Kward
|
|
|
1795
1850
|
@reserved_rows = 0
|
|
1796
1851
|
@last_width = current_width
|
|
1797
1852
|
@last_height = current_height
|
|
1798
|
-
redraw_screen_locked
|
|
1853
|
+
redraw_screen_locked(width: current_width, height: current_height)
|
|
1799
1854
|
true
|
|
1800
1855
|
end
|
|
1801
1856
|
|
|
@@ -1804,18 +1859,33 @@ module Kward
|
|
|
1804
1859
|
@reserved_rows = 0
|
|
1805
1860
|
end
|
|
1806
1861
|
|
|
1807
|
-
def render_composer_rows_locked(rows)
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
rows.
|
|
1862
|
+
def render_composer_rows_locked(rows, height: screen_height)
|
|
1863
|
+
top = composer_top_row(height)
|
|
1864
|
+
max_rows = [@last_composer_rows.length, rows.length].max
|
|
1865
|
+
rows_to_clear = [@reserved_rows - rows.length, 0].max
|
|
1866
|
+
|
|
1867
|
+
max_rows.times do |index|
|
|
1868
|
+
row = rows[index]
|
|
1869
|
+
previous = @last_composer_rows[index]
|
|
1870
|
+
next if row == previous
|
|
1871
|
+
|
|
1811
1872
|
move_to_screen(top + index, 1)
|
|
1812
|
-
@output_io.print(
|
|
1873
|
+
@output_io.print(TTY::Cursor.clear_line)
|
|
1874
|
+
@output_io.print(row) unless row.to_s.empty?
|
|
1813
1875
|
end
|
|
1876
|
+
|
|
1877
|
+
rows.length.upto(rows.length + rows_to_clear - 1) do |index|
|
|
1878
|
+
move_to_screen(top + index, 1)
|
|
1879
|
+
@output_io.print(TTY::Cursor.clear_line)
|
|
1880
|
+
end
|
|
1881
|
+
|
|
1882
|
+
@last_composer_rows = rows.dup
|
|
1814
1883
|
end
|
|
1815
1884
|
|
|
1816
|
-
def clear_composer_region_locked(rows_to_clear = nil)
|
|
1885
|
+
def clear_composer_region_locked(rows_to_clear = nil, height: screen_height)
|
|
1817
1886
|
rows_to_clear ||= [@reserved_rows, @rendered_rows].max
|
|
1818
|
-
clear_bottom_rows_locked(
|
|
1887
|
+
clear_bottom_rows_locked(height, rows_to_clear)
|
|
1888
|
+
@last_composer_rows = []
|
|
1819
1889
|
end
|
|
1820
1890
|
|
|
1821
1891
|
def resize_prompt_clear_rows(old_width, current_width, old_reserved_rows)
|
|
@@ -1838,7 +1908,7 @@ module Kward
|
|
|
1838
1908
|
def clear_bottom_rows_locked(height, rows_to_clear)
|
|
1839
1909
|
return unless rows_to_clear.positive?
|
|
1840
1910
|
|
|
1841
|
-
bottom =
|
|
1911
|
+
bottom = height
|
|
1842
1912
|
top = [bottom - rows_to_clear + 1, 1].max
|
|
1843
1913
|
clear_screen_rows_locked(top, bottom)
|
|
1844
1914
|
end
|
|
@@ -1850,32 +1920,33 @@ module Kward
|
|
|
1850
1920
|
end
|
|
1851
1921
|
end
|
|
1852
1922
|
|
|
1853
|
-
def redraw_screen_locked
|
|
1923
|
+
def redraw_screen_locked(width: screen_width, height: screen_height)
|
|
1854
1924
|
return unless @started
|
|
1855
1925
|
|
|
1856
1926
|
restore_scroll_region_locked
|
|
1857
1927
|
@output_io.print(TTY::Cursor.clear_screen)
|
|
1858
1928
|
move_to_screen(1, 1)
|
|
1859
1929
|
@reserved_rows = 0
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1930
|
+
@last_composer_rows = []
|
|
1931
|
+
rows, cursor_row, cursor_col = composer_layout(width, height)
|
|
1932
|
+
ensure_scroll_region_locked(rows.length, redraw_transcript: false, width: width, height: height)
|
|
1933
|
+
redraw_transcript_locked(width: width, height: height)
|
|
1863
1934
|
@rendered_rows = @asking ? rows.length : 0
|
|
1864
|
-
render_composer_rows_locked(rows) if @asking
|
|
1935
|
+
render_composer_rows_locked(rows, height: height) if @asking
|
|
1865
1936
|
@cursor_rendered_row = @asking ? cursor_row : 0
|
|
1866
|
-
@last_width =
|
|
1867
|
-
@last_height =
|
|
1868
|
-
reset_stream_position_from_transcript_locked
|
|
1937
|
+
@last_width = width
|
|
1938
|
+
@last_height = height
|
|
1939
|
+
reset_stream_position_from_transcript_locked(width)
|
|
1869
1940
|
if @asking
|
|
1870
|
-
move_to_screen(composer_top_row + cursor_row, cursor_col + 1)
|
|
1941
|
+
move_to_screen(composer_top_row(height) + cursor_row, cursor_col + 1)
|
|
1871
1942
|
render_cursor_visibility_locked
|
|
1872
1943
|
end
|
|
1873
1944
|
end
|
|
1874
1945
|
|
|
1875
|
-
def redraw_transcript_locked
|
|
1946
|
+
def redraw_transcript_locked(width: screen_width, height: screen_height)
|
|
1876
1947
|
return unless transcript_renderable?
|
|
1877
1948
|
|
|
1878
|
-
rows = transcript_viewport_rows(transcript_redraw_row_count,
|
|
1949
|
+
rows = transcript_viewport_rows(transcript_redraw_row_count(height), width)
|
|
1879
1950
|
clear_screen_rows_locked(1, rows.length)
|
|
1880
1951
|
return if rows.empty?
|
|
1881
1952
|
|
|
@@ -1895,12 +1966,12 @@ module Kward
|
|
|
1895
1966
|
rows
|
|
1896
1967
|
end
|
|
1897
1968
|
|
|
1898
|
-
def transcript_redraw_row_count
|
|
1899
|
-
[[@transcript_viewport_rows, transcript_bottom_row].max,
|
|
1969
|
+
def transcript_redraw_row_count(height = screen_height)
|
|
1970
|
+
[[@transcript_viewport_rows, transcript_bottom_row(height)].max, height].min
|
|
1900
1971
|
end
|
|
1901
1972
|
|
|
1902
|
-
def remember_transcript_viewport_locked
|
|
1903
|
-
@transcript_viewport_rows = transcript_bottom_row
|
|
1973
|
+
def remember_transcript_viewport_locked(height = screen_height)
|
|
1974
|
+
@transcript_viewport_rows = transcript_bottom_row(height)
|
|
1904
1975
|
end
|
|
1905
1976
|
|
|
1906
1977
|
def transcript_renderable?
|
|
@@ -1908,11 +1979,14 @@ module Kward
|
|
|
1908
1979
|
end
|
|
1909
1980
|
|
|
1910
1981
|
def transcript_display_rows(width)
|
|
1982
|
+
return @transcript_display_rows_cache if @transcript_display_rows_cache_width == width && @transcript_display_rows_cache
|
|
1983
|
+
|
|
1911
1984
|
rows = []
|
|
1912
1985
|
@visual_banner_count.times { rows.concat(banner_rows(width)) }
|
|
1913
1986
|
rows << "" if @visual_banner_count.positive? && @transcript_buffer.empty?
|
|
1914
1987
|
rows.concat(transcript_text_display_rows(width))
|
|
1915
|
-
|
|
1988
|
+
@transcript_display_rows_cache_width = width
|
|
1989
|
+
@transcript_display_rows_cache = rows
|
|
1916
1990
|
end
|
|
1917
1991
|
|
|
1918
1992
|
def transcript_text_display_rows(width)
|
|
@@ -1922,8 +1996,7 @@ module Kward
|
|
|
1922
1996
|
end
|
|
1923
1997
|
end
|
|
1924
1998
|
|
|
1925
|
-
def reset_stream_position_from_transcript_locked
|
|
1926
|
-
width = screen_width
|
|
1999
|
+
def reset_stream_position_from_transcript_locked(width = screen_width)
|
|
1927
2000
|
rows = transcript_display_rows(width)
|
|
1928
2001
|
last_length = rows.empty? ? 0 : ANSI.strip(rows.last).length
|
|
1929
2002
|
if last_length >= width
|
|
@@ -1935,34 +2008,34 @@ module Kward
|
|
|
1935
2008
|
end
|
|
1936
2009
|
end
|
|
1937
2010
|
|
|
1938
|
-
def move_to_transcript_cursor_locked
|
|
2011
|
+
def move_to_transcript_cursor_locked(width: screen_width, height: screen_height)
|
|
1939
2012
|
if @stream_pending_wrap
|
|
1940
|
-
move_to_screen(transcript_bottom_row,
|
|
2013
|
+
move_to_screen(transcript_bottom_row(height), width)
|
|
1941
2014
|
else
|
|
1942
|
-
move_to_screen(transcript_bottom_row, [@stream_col + 1,
|
|
2015
|
+
move_to_screen(transcript_bottom_row(height), [@stream_col + 1, width].min)
|
|
1943
2016
|
end
|
|
1944
2017
|
end
|
|
1945
2018
|
|
|
1946
|
-
def advance_pending_stream_wrap_locked(output_text)
|
|
2019
|
+
def advance_pending_stream_wrap_locked(output_text, width: screen_width, height: screen_height)
|
|
1947
2020
|
return unless @stream_pending_wrap
|
|
1948
2021
|
return if output_text.empty? || output_text.start_with?("\r", "\n")
|
|
1949
2022
|
|
|
1950
|
-
move_to_screen(transcript_bottom_row,
|
|
2023
|
+
move_to_screen(transcript_bottom_row(height), width)
|
|
1951
2024
|
@output_io.print("\r\n")
|
|
1952
2025
|
@stream_col = 0
|
|
1953
2026
|
@stream_pending_wrap = false
|
|
1954
2027
|
end
|
|
1955
2028
|
|
|
1956
|
-
def composer_layout(width)
|
|
1957
|
-
return compact_composer_layout(width) if
|
|
1958
|
-
return question_composer_layout(width) if @question_state
|
|
2029
|
+
def composer_layout(width, height = screen_height)
|
|
2030
|
+
return compact_composer_layout(width) if height < 4
|
|
2031
|
+
return question_composer_layout(width, height) if @question_state
|
|
1959
2032
|
|
|
1960
2033
|
content_width = [width - 4, 1].max
|
|
1961
2034
|
input_layout_rows, input_cursor_row, input_cursor_col = input_layout(content_width)
|
|
1962
2035
|
attachment_rows = attachment_badge_rows(content_width)
|
|
1963
|
-
overlay_rows = active_overlay_rows(width)
|
|
2036
|
+
overlay_rows = active_overlay_rows(width, height: height)
|
|
1964
2037
|
footer_text = footer_text()
|
|
1965
|
-
max_input_rows = max_visible_input_rows(attachment_rows.length, overlay_rows.length, footer_text.empty? ? 0 : 1)
|
|
2038
|
+
max_input_rows = max_visible_input_rows(attachment_rows.length, overlay_rows.length, footer_text.empty? ? 0 : 1, height: height)
|
|
1966
2039
|
visible_start = [[input_cursor_row - max_input_rows + 1, 0].max, [input_layout_rows.length - max_input_rows, 0].max].min
|
|
1967
2040
|
visible_rows = input_layout_rows[visible_start, max_input_rows] || [""]
|
|
1968
2041
|
rows = overlay_rows + [top_border(width)]
|
|
@@ -1975,75 +2048,28 @@ module Kward
|
|
|
1975
2048
|
[rows, cursor_row, cursor_col]
|
|
1976
2049
|
end
|
|
1977
2050
|
|
|
1978
|
-
def question_composer_layout(width)
|
|
2051
|
+
def question_composer_layout(width, height = screen_height)
|
|
1979
2052
|
content_width = [width - 4, 1].max
|
|
1980
|
-
overlay_rows = active_overlay_rows(width)
|
|
2053
|
+
overlay_rows = active_overlay_rows(width, height: height)
|
|
1981
2054
|
rows = overlay_rows + [top_border(width), box_content_row("", content_width), bottom_border(width)]
|
|
1982
2055
|
return [rows, question_custom_cursor_row, question_custom_cursor_col(width)] if selected_question_choice&.fetch(:custom, false)
|
|
1983
2056
|
|
|
1984
2057
|
[rows, overlay_rows.length + 1, 2]
|
|
1985
2058
|
end
|
|
1986
2059
|
|
|
1987
|
-
def active_overlay_rows(width)
|
|
2060
|
+
def active_overlay_rows(width, height: screen_height)
|
|
1988
2061
|
return question_overlay_rows(width) if @question_state
|
|
1989
|
-
return selection_overlay_rows(width) if @select_state
|
|
2062
|
+
return selection_overlay_rows(width, height: height) if @select_state
|
|
1990
2063
|
|
|
1991
|
-
slash_overlay_rows(width)
|
|
2064
|
+
slash_overlay_rows(width, height: height)
|
|
1992
2065
|
end
|
|
1993
2066
|
|
|
1994
2067
|
def banner_rows(width)
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
rows = []
|
|
1998
|
-
if banner_image_visible?
|
|
1999
|
-
rows.concat(centered_banner_image_rows(width))
|
|
2000
|
-
end
|
|
2001
|
-
rows << align_plain_row(@banner_message, width) unless @banner_message.empty?
|
|
2002
|
-
rows << ""
|
|
2003
|
-
rows
|
|
2004
|
-
end
|
|
2005
|
-
|
|
2006
|
-
def banner_visible?
|
|
2007
|
-
!@banner_message.empty? || banner_image_visible?
|
|
2008
|
-
end
|
|
2009
|
-
|
|
2010
|
-
def banner_image_visible?
|
|
2011
|
-
!banner_logo_rows.empty?
|
|
2012
|
-
end
|
|
2013
|
-
|
|
2014
|
-
def centered_banner_image_rows(width)
|
|
2015
|
-
logo_width, = banner_logo_dimensions(width)
|
|
2016
|
-
padding = [[(width - logo_width) / 2, 0].max, width - 1].min
|
|
2017
|
-
banner_logo_rows.map { |row| (" " * padding) + row }
|
|
2068
|
+
@banner.rows(width)
|
|
2018
2069
|
end
|
|
2019
2070
|
|
|
2020
2071
|
def banner_logo_rows
|
|
2021
|
-
|
|
2022
|
-
return [] unless @banner_logo_pixels && max_banner_logo_height >= BANNER_MIN_LOGO_HEIGHT
|
|
2023
|
-
|
|
2024
|
-
key = [logo_width, logo_height]
|
|
2025
|
-
@banner_logo_cache[key] ||= Kward::PixelLogo.half_block_rows_from_pixels(@banner_logo_pixels, width: logo_width, pixel_height: logo_height)
|
|
2026
|
-
end
|
|
2027
|
-
|
|
2028
|
-
def banner_logo_dimensions(width)
|
|
2029
|
-
logo_width = [BANNER_LOGO_WIDTH, [width - 2, 1].max].min
|
|
2030
|
-
logo_height = [BANNER_LOGO_PIXEL_HEIGHT, max_banner_logo_height * 2].min
|
|
2031
|
-
[logo_width, logo_height]
|
|
2032
|
-
end
|
|
2033
|
-
|
|
2034
|
-
def max_banner_logo_height
|
|
2035
|
-
message_rows = @banner_message.empty? ? 0 : 1
|
|
2036
|
-
blank_after_banner = 1
|
|
2037
|
-
minimum_composer_rows = 3
|
|
2038
|
-
transcript_row = 1
|
|
2039
|
-
reserved_rows = message_rows + blank_after_banner + minimum_composer_rows + transcript_row
|
|
2040
|
-
[screen_height - reserved_rows, 0].max
|
|
2041
|
-
end
|
|
2042
|
-
|
|
2043
|
-
def align_plain_row(text, width)
|
|
2044
|
-
plain_length = ANSI.strip(text).length
|
|
2045
|
-
padding = [width - plain_length, 0].max / 2
|
|
2046
|
-
(" " * padding) + text.to_s
|
|
2072
|
+
@banner.logo_rows(screen_width)
|
|
2047
2073
|
end
|
|
2048
2074
|
|
|
2049
2075
|
def question_overlay_rows(width)
|
|
@@ -2060,10 +2086,10 @@ module Kward
|
|
|
2060
2086
|
overlay_card_rows(title, lines, width)
|
|
2061
2087
|
end
|
|
2062
2088
|
|
|
2063
|
-
def slash_overlay_rows(width)
|
|
2089
|
+
def slash_overlay_rows(width, height: screen_height)
|
|
2064
2090
|
return [] unless slash_overlay_visible?
|
|
2065
2091
|
|
|
2066
|
-
visible = visible_slash_overlay_matches(slash_overlay_matches)
|
|
2092
|
+
visible = visible_slash_overlay_matches(slash_overlay_matches, height: height)
|
|
2067
2093
|
start_index = visible[:start]
|
|
2068
2094
|
lines = visible[:commands].each_with_index.map do |command, offset|
|
|
2069
2095
|
index = start_index + offset
|
|
@@ -2074,13 +2100,13 @@ module Kward
|
|
|
2074
2100
|
overlay_card_rows("Slash commands", lines, width)
|
|
2075
2101
|
end
|
|
2076
2102
|
|
|
2077
|
-
def visible_slash_overlay_matches(matches)
|
|
2078
|
-
max_rows = [[
|
|
2103
|
+
def visible_slash_overlay_matches(matches, height: screen_height)
|
|
2104
|
+
max_rows = [[height - 7, 1].max, 8].min
|
|
2079
2105
|
start = [[@slash_selection_index - max_rows + 1, 0].max, [matches.length - max_rows, 0].max].min
|
|
2080
2106
|
{ start: start, commands: matches[start, max_rows] || [] }
|
|
2081
2107
|
end
|
|
2082
2108
|
|
|
2083
|
-
def selection_overlay_rows(width)
|
|
2109
|
+
def selection_overlay_rows(width, height: screen_height)
|
|
2084
2110
|
matches = selection_matches
|
|
2085
2111
|
lines = [overlay_text_line("↑/↓ select · Enter open · Esc cancel", :muted), overlay_blank_line]
|
|
2086
2112
|
if matches.empty?
|
|
@@ -2092,7 +2118,7 @@ module Kward
|
|
|
2092
2118
|
return overlay_card_rows(selection_overlay_title, lines, width)
|
|
2093
2119
|
end
|
|
2094
2120
|
|
|
2095
|
-
visible = visible_selection_matches(matches)
|
|
2121
|
+
visible = visible_selection_matches(matches, height: height)
|
|
2096
2122
|
start_index = visible[:start]
|
|
2097
2123
|
visible[:choices].each_with_index do |choice, offset|
|
|
2098
2124
|
index = start_index + offset
|
|
@@ -2106,8 +2132,8 @@ module Kward
|
|
|
2106
2132
|
title && !title.empty? ? title : "Sessions"
|
|
2107
2133
|
end
|
|
2108
2134
|
|
|
2109
|
-
def visible_selection_matches(matches)
|
|
2110
|
-
max_rows = [[
|
|
2135
|
+
def visible_selection_matches(matches, height: screen_height)
|
|
2136
|
+
max_rows = [[height - 7, 1].max, 8].min
|
|
2111
2137
|
start = [[selection_index - max_rows + 1, 0].max, [matches.length - max_rows, 0].max].min
|
|
2112
2138
|
{ start: start, choices: matches[start, max_rows] || [] }
|
|
2113
2139
|
end
|
|
@@ -2354,17 +2380,17 @@ module Kward
|
|
|
2354
2380
|
[]
|
|
2355
2381
|
end
|
|
2356
2382
|
|
|
2357
|
-
def max_visible_input_rows(attachment_count = 0, overlay_count = active_overlay_rows(screen_width).length, footer_count = footer_text.to_s.empty? ? 0 : 1)
|
|
2383
|
+
def max_visible_input_rows(attachment_count = 0, overlay_count = active_overlay_rows(screen_width).length, footer_count = footer_text.to_s.empty? ? 0 : 1, height: screen_height)
|
|
2358
2384
|
input_cap = [COMPOSER_MAX_INPUT_ROWS - attachment_count, 1].max
|
|
2359
|
-
[[input_cap,
|
|
2385
|
+
[[input_cap, height - 3 - overlay_count - footer_count - attachment_count].min, 1].max
|
|
2360
2386
|
end
|
|
2361
2387
|
|
|
2362
|
-
def composer_top_row
|
|
2363
|
-
[
|
|
2388
|
+
def composer_top_row(height = screen_height)
|
|
2389
|
+
[height - @reserved_rows + 1, 1].max
|
|
2364
2390
|
end
|
|
2365
2391
|
|
|
2366
|
-
def transcript_bottom_row
|
|
2367
|
-
[
|
|
2392
|
+
def transcript_bottom_row(height = screen_height)
|
|
2393
|
+
[height - @reserved_rows, 1].max
|
|
2368
2394
|
end
|
|
2369
2395
|
|
|
2370
2396
|
def move_to_screen(row, col)
|
|
@@ -2385,8 +2411,7 @@ module Kward
|
|
|
2385
2411
|
[before_cursor.count("\n"), (before_cursor.split("\n", -1).last || "").length]
|
|
2386
2412
|
end
|
|
2387
2413
|
|
|
2388
|
-
def update_stream_position(text)
|
|
2389
|
-
width = screen_width
|
|
2414
|
+
def update_stream_position(text, width: screen_width)
|
|
2390
2415
|
ANSI.strip(text).each_char do |char|
|
|
2391
2416
|
case char
|
|
2392
2417
|
when "\n", "\r"
|
|
@@ -2426,6 +2451,10 @@ module Kward
|
|
|
2426
2451
|
end
|
|
2427
2452
|
end
|
|
2428
2453
|
|
|
2454
|
+
def screen_size
|
|
2455
|
+
[screen_width, screen_height]
|
|
2456
|
+
end
|
|
2457
|
+
|
|
2429
2458
|
def screen_width
|
|
2430
2459
|
[TTY::Screen.width, 1].max
|
|
2431
2460
|
end
|