kward 0.81.0 → 0.83.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 +57 -0
- data/Gemfile.lock +2 -2
- data/README.md +1 -1
- data/Rakefile +44 -4
- data/doc/agent-tools.md +1 -0
- data/doc/composer.md +2 -2
- data/doc/configuration.md +18 -3
- data/doc/editor.md +20 -10
- data/doc/files.md +12 -5
- data/doc/permissions.md +1 -0
- data/doc/releasing.md +79 -38
- data/doc/rpc.md +2 -2
- data/doc/sandboxing.md +5 -1
- data/doc/security.md +2 -2
- data/doc/shell.md +42 -18
- data/doc/tabs.md +3 -0
- data/doc/usage.md +3 -2
- data/kward.gemspec +1 -1
- data/lib/kward/adaptive_pty_output_sink.rb +183 -0
- data/lib/kward/agent.rb +1 -0
- data/lib/kward/auth/anthropic_oauth.rb +7 -7
- data/lib/kward/cli/git.rb +5 -1
- data/lib/kward/cli/interactive_turn.rb +101 -19
- data/lib/kward/cli/rendering.rb +14 -2
- data/lib/kward/cli/runtime_helpers.rb +252 -46
- data/lib/kward/cli/settings.rb +5 -13
- data/lib/kward/cli/slash_commands.rb +12 -1
- data/lib/kward/cli/tabs.rb +71 -9
- data/lib/kward/cli.rb +12 -0
- data/lib/kward/compaction/token_estimator.rb +12 -6
- data/lib/kward/config_files.rb +17 -0
- data/lib/kward/editor_prompt.rb +46 -0
- data/lib/kward/editor_prompt_session.rb +28 -0
- data/lib/kward/ekwsh.rb +70 -10
- data/lib/kward/image_attachments.rb +98 -15
- data/lib/kward/interactive_pty_runner.rb +25 -19
- data/lib/kward/model/client.rb +1 -17
- data/lib/kward/model/model_info.rb +3 -2
- data/lib/kward/model/payloads.rb +0 -2
- data/lib/kward/persistent_shell_session.rb +750 -0
- data/lib/kward/project_files.rb +18 -5
- data/lib/kward/prompt_interface/composer_controller.rb +3 -3
- data/lib/kward/prompt_interface/composer_renderer.rb +28 -1
- data/lib/kward/prompt_interface/editor/auto_indent.rb +3 -0
- data/lib/kward/prompt_interface/editor/controller.rb +75 -6
- data/lib/kward/prompt_interface/editor/modes/emacs.rb +2 -2
- data/lib/kward/prompt_interface/editor/modes/modern.rb +4 -0
- data/lib/kward/prompt_interface/editor/modes/vibe.rb +12 -6
- data/lib/kward/prompt_interface/editor/renderer.rb +1 -0
- data/lib/kward/prompt_interface/editor/state.rb +11 -0
- data/lib/kward/prompt_interface/editor/word_completion.rb +124 -0
- data/lib/kward/prompt_interface/file_overlay.rb +21 -7
- data/lib/kward/prompt_interface/git_prompt.rb +1 -1
- data/lib/kward/prompt_interface/key_handler.rb +30 -0
- data/lib/kward/prompt_interface/overlay_renderer.rb +1 -0
- data/lib/kward/prompt_interface/project_browser.rb +192 -8
- data/lib/kward/prompt_interface/prompt_renderer.rb +15 -6
- data/lib/kward/prompt_interface/question_prompt.rb +1 -1
- data/lib/kward/prompt_interface/runtime_state.rb +5 -1
- data/lib/kward/prompt_interface/screen.rb +40 -20
- data/lib/kward/prompt_interface/selection_prompt.rb +5 -5
- data/lib/kward/prompt_interface/transcript_renderer.rb +4 -4
- data/lib/kward/prompt_interface.rb +159 -32
- data/lib/kward/prompts/commands.rb +1 -0
- data/lib/kward/pty_output_sink.rb +45 -0
- data/lib/kward/pty_transcript_normalizer.rb +93 -0
- data/lib/kward/rpc/auth_manager.rb +1 -1
- data/lib/kward/rpc/server.rb +2 -1
- data/lib/kward/session_catalog.rb +87 -0
- data/lib/kward/session_store.rb +94 -5
- data/lib/kward/shell_prompt.rb +50 -0
- data/lib/kward/shell_prompt_session.rb +58 -0
- data/lib/kward/terminal_image_support.rb +116 -0
- data/lib/kward/terminal_keys.rb +1 -0
- data/lib/kward/terminal_sequences.rb +43 -0
- data/lib/kward/tools/prepare_shell_command.rb +28 -0
- data/lib/kward/tools/registry.rb +63 -5
- data/lib/kward/tools/replace_editor_buffer.rb +30 -0
- data/lib/kward/tools/run_shell_command.rb +24 -6
- data/lib/kward/version.rb +1 -1
- data/templates/default/layout/html/layout.erb +1 -1
- metadata +15 -4
- data/.github/workflows/ci.yml +0 -48
- data/.github/workflows/pages.yml +0 -48
|
@@ -34,6 +34,10 @@ module Kward
|
|
|
34
34
|
}.freeze
|
|
35
35
|
PROJECT_BROWSER_DIRECTORY_ICON = ""
|
|
36
36
|
PROJECT_BROWSER_FILE_ICON = ""
|
|
37
|
+
IMAGE_VIEWER_CELL_HEIGHT_TO_WIDTH = 2.0
|
|
38
|
+
IMAGE_VIEWER_ZOOM_STEP = 0.25
|
|
39
|
+
IMAGE_VIEWER_MIN_ZOOM = 0.25
|
|
40
|
+
IMAGE_VIEWER_MAX_ZOOM = 4.0
|
|
37
41
|
|
|
38
42
|
def open_project_browser
|
|
39
43
|
@mutex.synchronize do
|
|
@@ -44,14 +48,17 @@ module Kward
|
|
|
44
48
|
end
|
|
45
49
|
|
|
46
50
|
def open_project_browser_locked
|
|
47
|
-
paths = project_file_paths
|
|
48
51
|
saved_state = saved_project_browser_state
|
|
52
|
+
show_ignored = saved_state["show_ignored"] == true
|
|
53
|
+
paths = project_file_paths(include_ignored: show_ignored)
|
|
49
54
|
@project_browser_state = {
|
|
50
55
|
paths: paths,
|
|
56
|
+
show_ignored: show_ignored,
|
|
51
57
|
expanded: restored_project_browser_expanded_paths(paths, saved_state),
|
|
52
58
|
selection_index: 0,
|
|
53
59
|
search_active: false,
|
|
54
|
-
query: ""
|
|
60
|
+
query: "",
|
|
61
|
+
status: nil
|
|
55
62
|
}
|
|
56
63
|
restore_project_browser_selection(saved_state["selected_path"])
|
|
57
64
|
self.composer_input = ""
|
|
@@ -63,7 +70,11 @@ module Kward
|
|
|
63
70
|
private
|
|
64
71
|
|
|
65
72
|
def project_browser_visible?
|
|
66
|
-
!@project_browser_state.nil? && !editor_active?
|
|
73
|
+
!@project_browser_state.nil? && !editor_active? && !image_viewer_active?
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def image_viewer_active?
|
|
77
|
+
!@image_viewer_state.nil?
|
|
67
78
|
end
|
|
68
79
|
|
|
69
80
|
def dismiss_project_browser
|
|
@@ -133,6 +144,8 @@ module Kward
|
|
|
133
144
|
collapse_selected_project_browser_row
|
|
134
145
|
elsif text == "l" && !project_browser_search_active?
|
|
135
146
|
expand_selected_project_browser_row
|
|
147
|
+
elsif text == "i" && !project_browser_search_active?
|
|
148
|
+
toggle_project_browser_ignored_files
|
|
136
149
|
elsif project_browser_search_active?
|
|
137
150
|
project_browser_append_search(text)
|
|
138
151
|
else
|
|
@@ -164,6 +177,8 @@ module Kward
|
|
|
164
177
|
project_browser_search_active? ? project_browser_append_search(key) : collapse_selected_project_browser_row
|
|
165
178
|
when "l"
|
|
166
179
|
project_browser_search_active? ? project_browser_append_search(key) : expand_selected_project_browser_row
|
|
180
|
+
when "i"
|
|
181
|
+
project_browser_search_active? ? project_browser_append_search(key) : toggle_project_browser_ignored_files
|
|
167
182
|
else
|
|
168
183
|
project_browser_append_search(key) if project_browser_search_active? && printable_key?(key)
|
|
169
184
|
end
|
|
@@ -181,6 +196,21 @@ module Kward
|
|
|
181
196
|
project_browser_search_active? ? deactivate_project_browser_search : activate_project_browser_search
|
|
182
197
|
end
|
|
183
198
|
|
|
199
|
+
def toggle_project_browser_ignored_files
|
|
200
|
+
selected_path = selected_project_browser_row&.[](:path)
|
|
201
|
+
show_ignored = !project_browser_ignored_files_visible?
|
|
202
|
+
@project_browser_state[:paths] = project_file_paths(include_ignored: show_ignored)
|
|
203
|
+
@project_browser_state[:show_ignored] = show_ignored
|
|
204
|
+
@project_browser_tree_paths = nil
|
|
205
|
+
@project_browser_tree = nil
|
|
206
|
+
restore_project_browser_selection(selected_path)
|
|
207
|
+
persist_project_browser_state
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def project_browser_ignored_files_visible?
|
|
211
|
+
@project_browser_state[:show_ignored] == true
|
|
212
|
+
end
|
|
213
|
+
|
|
184
214
|
def activate_project_browser_search
|
|
185
215
|
@project_browser_state[:search_active] = true
|
|
186
216
|
@project_browser_state[:query] = ""
|
|
@@ -243,6 +273,8 @@ module Kward
|
|
|
243
273
|
if row[:directory]
|
|
244
274
|
toggle_project_browser_directory(row[:path])
|
|
245
275
|
true
|
|
276
|
+
elsif Kward::ImageAttachments.image_extension?(row[:path])
|
|
277
|
+
open_project_browser_image(row[:path])
|
|
246
278
|
else
|
|
247
279
|
persist_project_browser_state unless project_browser_search_active?
|
|
248
280
|
@project_browser_restore_after_editor = true if open_editor(row[:path])
|
|
@@ -250,6 +282,152 @@ module Kward
|
|
|
250
282
|
end
|
|
251
283
|
end
|
|
252
284
|
|
|
285
|
+
def open_project_browser_image(path)
|
|
286
|
+
full_path = File.expand_path(path.to_s, prompt_workspace_root)
|
|
287
|
+
part = Kward::ImageAttachments.image_part_from_path(full_path)
|
|
288
|
+
unless part
|
|
289
|
+
@project_browser_state[:status] = "Cannot preview #{path}: unreadable or larger than 20 MB"
|
|
290
|
+
return false
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
protocol = terminal_image_protocol
|
|
294
|
+
unless protocol
|
|
295
|
+
@project_browser_state[:status] = "Cannot preview #{path}: terminal does not support inline images"
|
|
296
|
+
return false
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
part = Kward::ImageAttachments.terminal_image_part(part, protocol)
|
|
300
|
+
unless part
|
|
301
|
+
@project_browser_state[:status] = "Cannot preview #{path}: terminal cannot render this image format"
|
|
302
|
+
return false
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
image_id = next_terminal_image_id
|
|
306
|
+
columns = image_viewer_columns(screen_width)
|
|
307
|
+
rows = image_viewer_preview_rows(screen_height, width: screen_width)
|
|
308
|
+
terminal_columns, terminal_rows = image_viewer_terminal_size(part, protocol, columns, rows)
|
|
309
|
+
sequence = Kward::ImageAttachments.terminal_image_sequence(
|
|
310
|
+
part,
|
|
311
|
+
width: terminal_columns,
|
|
312
|
+
height: terminal_rows,
|
|
313
|
+
protocol: protocol,
|
|
314
|
+
image_id: image_id,
|
|
315
|
+
move_cursor: false,
|
|
316
|
+
env: ENV
|
|
317
|
+
)
|
|
318
|
+
unless sequence
|
|
319
|
+
@project_browser_state[:status] = "Cannot preview #{path}: terminal cannot render this image format"
|
|
320
|
+
return false
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
@image_viewer_state = {
|
|
324
|
+
path: full_path,
|
|
325
|
+
display_path: path.to_s,
|
|
326
|
+
protocol: protocol,
|
|
327
|
+
image_id: image_id,
|
|
328
|
+
part: part,
|
|
329
|
+
zoom: 1.0,
|
|
330
|
+
columns: columns,
|
|
331
|
+
rows: rows,
|
|
332
|
+
sequence: sequence
|
|
333
|
+
}
|
|
334
|
+
@pending_keys.clear
|
|
335
|
+
@asking = true
|
|
336
|
+
true
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def close_image_viewer
|
|
340
|
+
return false unless image_viewer_active?
|
|
341
|
+
|
|
342
|
+
clear_image_viewer_output_locked
|
|
343
|
+
@image_viewer_state = nil
|
|
344
|
+
sync_project_browser_query_input
|
|
345
|
+
@asking = true
|
|
346
|
+
true
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def terminal_image_protocol
|
|
350
|
+
return @terminal_image_protocol if @terminal_image_protocol
|
|
351
|
+
|
|
352
|
+
detected = TerminalImageSupport.detect(input: @input_io, output: @output_io)
|
|
353
|
+
@terminal_image_protocol = detected if detected
|
|
354
|
+
detected
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
def next_terminal_image_id
|
|
358
|
+
@next_terminal_image_id ||= 0
|
|
359
|
+
@next_terminal_image_id += 1
|
|
360
|
+
@next_terminal_image_id = 1 if @next_terminal_image_id > 4_294_967_295
|
|
361
|
+
@next_terminal_image_id
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def image_viewer_zoom
|
|
365
|
+
@image_viewer_state&.fetch(:zoom, 1.0) || 1.0
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
def zoom_image_viewer(direction)
|
|
369
|
+
return false unless image_viewer_active?
|
|
370
|
+
|
|
371
|
+
delta = direction == :in ? IMAGE_VIEWER_ZOOM_STEP : -IMAGE_VIEWER_ZOOM_STEP
|
|
372
|
+
zoom = [[image_viewer_zoom + delta, IMAGE_VIEWER_MIN_ZOOM].max, IMAGE_VIEWER_MAX_ZOOM].min
|
|
373
|
+
@image_viewer_state[:zoom] = zoom.round(2)
|
|
374
|
+
@asking = true
|
|
375
|
+
true
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
def image_viewer_columns(width = screen_width)
|
|
379
|
+
available_columns = [width - 4, 1].max
|
|
380
|
+
desired_columns = [(ImageAttachments::DEFAULT_TERMINAL_IMAGE_WIDTH.to_i * image_viewer_zoom).round, 1].max
|
|
381
|
+
[desired_columns, available_columns].min
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
def image_viewer_sequence(width, height)
|
|
385
|
+
columns = image_viewer_columns(width)
|
|
386
|
+
rows = image_viewer_preview_rows(height, width: width)
|
|
387
|
+
state = @image_viewer_state
|
|
388
|
+
return state[:sequence] if state[:columns] == columns && state[:rows] == rows
|
|
389
|
+
|
|
390
|
+
terminal_columns, terminal_rows = image_viewer_terminal_size(state[:part], state[:protocol], columns, rows)
|
|
391
|
+
state[:columns] = columns
|
|
392
|
+
state[:rows] = rows
|
|
393
|
+
sequence = Kward::ImageAttachments.terminal_image_sequence(
|
|
394
|
+
state[:part],
|
|
395
|
+
width: terminal_columns,
|
|
396
|
+
height: terminal_rows,
|
|
397
|
+
protocol: state[:protocol],
|
|
398
|
+
image_id: state[:image_id],
|
|
399
|
+
move_cursor: false,
|
|
400
|
+
env: ENV
|
|
401
|
+
)
|
|
402
|
+
delete_sequence = image_viewer_delete_sequence(state[:image_id]) if state[:protocol] == TerminalImageSupport::KITTY
|
|
403
|
+
state[:sequence] = "#{delete_sequence}#{sequence}"
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
def image_viewer_terminal_size(part, protocol, columns, rows)
|
|
407
|
+
return [columns, rows] unless protocol == TerminalImageSupport::KITTY
|
|
408
|
+
|
|
409
|
+
dimensions = Kward::ImageAttachments.png_dimensions(part)
|
|
410
|
+
return [nil, rows] unless dimensions
|
|
411
|
+
|
|
412
|
+
image_width, image_height = dimensions
|
|
413
|
+
image_aspect_ratio = image_width.to_f / image_height
|
|
414
|
+
available_aspect_ratio = columns.to_f / (rows * IMAGE_VIEWER_CELL_HEIGHT_TO_WIDTH)
|
|
415
|
+
image_aspect_ratio > available_aspect_ratio ? [columns, nil] : [nil, rows]
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
def image_viewer_delete_sequence(image_id)
|
|
419
|
+
sequence = TerminalSequences.kitty_delete(image_id)
|
|
420
|
+
ENV["TMUX"].to_s.empty? ? sequence : TerminalSequences.tmux_passthrough(sequence)
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
def clear_image_viewer_output_locked
|
|
424
|
+
return unless image_viewer_active?
|
|
425
|
+
return unless @image_viewer_state[:protocol] == TerminalImageSupport::KITTY
|
|
426
|
+
return unless @started
|
|
427
|
+
|
|
428
|
+
print_output_locked(image_viewer_delete_sequence(@image_viewer_state[:image_id]))
|
|
429
|
+
end
|
|
430
|
+
|
|
253
431
|
def expand_selected_project_browser_row
|
|
254
432
|
row = selected_project_browser_row
|
|
255
433
|
return false unless row&.fetch(:directory, false)
|
|
@@ -311,14 +489,17 @@ module Kward
|
|
|
311
489
|
|
|
312
490
|
@project_browser_restore_after_editor = false
|
|
313
491
|
unless @project_browser_state
|
|
314
|
-
paths = project_file_paths
|
|
315
492
|
saved_state = saved_project_browser_state
|
|
493
|
+
show_ignored = saved_state["show_ignored"] == true
|
|
494
|
+
paths = project_file_paths(include_ignored: show_ignored)
|
|
316
495
|
@project_browser_state = {
|
|
317
496
|
paths: paths,
|
|
497
|
+
show_ignored: show_ignored,
|
|
318
498
|
expanded: restored_project_browser_expanded_paths(paths, saved_state),
|
|
319
499
|
selection_index: 0,
|
|
320
500
|
search_active: false,
|
|
321
|
-
query: ""
|
|
501
|
+
query: "",
|
|
502
|
+
status: nil
|
|
322
503
|
}
|
|
323
504
|
restore_project_browser_selection(saved_state["selected_path"])
|
|
324
505
|
end
|
|
@@ -341,6 +522,7 @@ module Kward
|
|
|
341
522
|
lines << overlay_choice_line(project_browser_row_text(row), selected: index == @project_browser_state[:selection_index])
|
|
342
523
|
end
|
|
343
524
|
end
|
|
525
|
+
lines << overlay_text_line(@project_browser_state[:status], :muted) if @project_browser_state[:status]
|
|
344
526
|
lines << overlay_blank_line
|
|
345
527
|
lines << overlay_text_line(project_browser_help_text, :muted)
|
|
346
528
|
overlay_card_rows(title, lines, width)
|
|
@@ -348,7 +530,8 @@ module Kward
|
|
|
348
530
|
|
|
349
531
|
def project_browser_title
|
|
350
532
|
query = @project_browser_state[:query].to_s
|
|
351
|
-
project_browser_search_active? ? "Project files — Search: #{query}" : "Project files"
|
|
533
|
+
title = project_browser_search_active? ? "Project files — Search: #{query}" : "Project files"
|
|
534
|
+
project_browser_ignored_files_visible? ? "#{title} (including ignored)" : title
|
|
352
535
|
end
|
|
353
536
|
|
|
354
537
|
def project_browser_empty_message
|
|
@@ -359,7 +542,7 @@ module Kward
|
|
|
359
542
|
if project_browser_search_active?
|
|
360
543
|
"Type search • Esc tree • Enter open • @ mention"
|
|
361
544
|
else
|
|
362
|
-
"Enter open/toggle • ←/→ collapse/expand • Tab or / search • @ mention • Esc close"
|
|
545
|
+
"Enter open/toggle • i show/hide ignored • ←/→ collapse/expand • Tab or / search • @ mention • Esc close"
|
|
363
546
|
end
|
|
364
547
|
end
|
|
365
548
|
|
|
@@ -472,7 +655,8 @@ module Kward
|
|
|
472
655
|
row = selected_project_browser_row
|
|
473
656
|
workspaces[project_browser_workspace_root] = {
|
|
474
657
|
"expanded" => @project_browser_state[:expanded].to_a.sort,
|
|
475
|
-
"selected_path" => row && row[:path]
|
|
658
|
+
"selected_path" => row && row[:path],
|
|
659
|
+
"show_ignored" => project_browser_ignored_files_visible?
|
|
476
660
|
}
|
|
477
661
|
data["version"] = PROJECT_BROWSER_STATE_VERSION
|
|
478
662
|
data["workspaces"] = workspaces
|
|
@@ -7,12 +7,12 @@ module Kward
|
|
|
7
7
|
private
|
|
8
8
|
|
|
9
9
|
def render_prompt_locked(synchronized: false)
|
|
10
|
-
return unless @started && @asking
|
|
10
|
+
return unless @started && @asking && !terminal_owned_by_child_locked?
|
|
11
11
|
|
|
12
12
|
width, height = screen_size
|
|
13
13
|
if width != @last_width || height != @last_height
|
|
14
14
|
with_synchronized_output_locked { render_prompt_body_locked }
|
|
15
|
-
|
|
15
|
+
flush_output_locked
|
|
16
16
|
return
|
|
17
17
|
end
|
|
18
18
|
|
|
@@ -23,10 +23,12 @@ module Kward
|
|
|
23
23
|
else
|
|
24
24
|
render_prompt_layout_locked(rows, cursor_row, cursor_col, width, height)
|
|
25
25
|
end
|
|
26
|
-
|
|
26
|
+
flush_output_locked
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
def render_prompt_body_locked
|
|
30
|
+
return if terminal_owned_by_child_locked?
|
|
31
|
+
|
|
30
32
|
handle_resize_locked
|
|
31
33
|
width, height = screen_size
|
|
32
34
|
rows, cursor_row, cursor_col = composer_layout(width, height)
|
|
@@ -49,6 +51,8 @@ module Kward
|
|
|
49
51
|
end
|
|
50
52
|
|
|
51
53
|
def clear_prompt_locked
|
|
54
|
+
return if terminal_owned_by_child_locked?
|
|
55
|
+
|
|
52
56
|
handle_resize_locked
|
|
53
57
|
width, height = screen_size
|
|
54
58
|
clear_composer_region_locked(height: height)
|
|
@@ -58,6 +62,8 @@ module Kward
|
|
|
58
62
|
end
|
|
59
63
|
|
|
60
64
|
def clear_prompt_for_output_locked
|
|
65
|
+
return if terminal_owned_by_child_locked?
|
|
66
|
+
|
|
61
67
|
handle_resize_locked
|
|
62
68
|
width, height = screen_size
|
|
63
69
|
reserve_composer_region_locked(width: width, height: height) if @started && @asking
|
|
@@ -68,6 +74,8 @@ module Kward
|
|
|
68
74
|
end
|
|
69
75
|
|
|
70
76
|
def prepare_transcript_output_locked
|
|
77
|
+
return if terminal_owned_by_child_locked?
|
|
78
|
+
|
|
71
79
|
handle_resize_locked
|
|
72
80
|
width, height = screen_size
|
|
73
81
|
hide_cursor_for_transcript_output_locked
|
|
@@ -76,7 +84,7 @@ module Kward
|
|
|
76
84
|
end
|
|
77
85
|
|
|
78
86
|
def restore_composer_cursor_locked
|
|
79
|
-
return unless @started && @asking
|
|
87
|
+
return unless @started && @asking && !terminal_owned_by_child_locked?
|
|
80
88
|
|
|
81
89
|
width, height = screen_size
|
|
82
90
|
_rows, cursor_row, cursor_col = composer_layout(width, height)
|
|
@@ -85,10 +93,11 @@ module Kward
|
|
|
85
93
|
end
|
|
86
94
|
|
|
87
95
|
def redraw_screen_locked(width: screen_width, height: screen_height)
|
|
88
|
-
return unless @started
|
|
96
|
+
return unless @started && !terminal_owned_by_child_locked?
|
|
89
97
|
|
|
98
|
+
clear_image_viewer_output_locked if image_viewer_active?
|
|
90
99
|
restore_scroll_region_locked
|
|
91
|
-
|
|
100
|
+
print_output_locked(TTY::Cursor.clear_screen)
|
|
92
101
|
move_to_screen(1, 1)
|
|
93
102
|
@reserved_rows = 0
|
|
94
103
|
@last_composer_rows = []
|
|
@@ -118,8 +118,12 @@ module Kward
|
|
|
118
118
|
result.is_a?(Hash) && result[:reasoning_action]
|
|
119
119
|
end
|
|
120
120
|
|
|
121
|
+
def editor_prompt_action?(result)
|
|
122
|
+
result.is_a?(Hash) && result[:editor_prompt]
|
|
123
|
+
end
|
|
124
|
+
|
|
121
125
|
def prompt_action_result?(result)
|
|
122
|
-
tab_action_result?(result) || reasoning_action_result?(result)
|
|
126
|
+
tab_action_result?(result) || reasoning_action_result?(result) || editor_prompt_action?(result)
|
|
123
127
|
end
|
|
124
128
|
|
|
125
129
|
end
|
|
@@ -6,6 +6,28 @@ module Kward
|
|
|
6
6
|
module Screen
|
|
7
7
|
private
|
|
8
8
|
|
|
9
|
+
def terminal_owned_by_child_locked?
|
|
10
|
+
@terminal_owner == :child
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def print_output_locked(*values)
|
|
14
|
+
return if terminal_owned_by_child_locked?
|
|
15
|
+
|
|
16
|
+
values.each { |value| @output_io.print(value) }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def puts_output_locked(*values)
|
|
20
|
+
return if terminal_owned_by_child_locked?
|
|
21
|
+
|
|
22
|
+
@output_io.puts(*values)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def flush_output_locked
|
|
26
|
+
return if terminal_owned_by_child_locked?
|
|
27
|
+
|
|
28
|
+
@output_io.flush
|
|
29
|
+
end
|
|
30
|
+
|
|
9
31
|
def enter_raw_mode_locked
|
|
10
32
|
return unless @input_io.respond_to?(:tty?) && @input_io.tty?
|
|
11
33
|
return unless @input_io.respond_to?(:console_mode) && @input_io.respond_to?(:console_mode=)
|
|
@@ -31,19 +53,19 @@ module Kward
|
|
|
31
53
|
end
|
|
32
54
|
|
|
33
55
|
def with_synchronized_output_locked
|
|
34
|
-
if @restoring_transcript || @synchronized_output_depth.positive?
|
|
56
|
+
if @restoring_transcript || @synchronized_output_depth.positive? || terminal_owned_by_child_locked?
|
|
35
57
|
yield
|
|
36
58
|
return
|
|
37
59
|
end
|
|
38
60
|
|
|
39
61
|
synchronized = true
|
|
40
62
|
@synchronized_output_depth += 1
|
|
41
|
-
|
|
63
|
+
print_output_locked(SYNCHRONIZED_OUTPUT_ENABLE)
|
|
42
64
|
yield
|
|
43
65
|
ensure
|
|
44
66
|
if synchronized
|
|
45
67
|
@synchronized_output_depth -= 1
|
|
46
|
-
|
|
68
|
+
print_output_locked(SYNCHRONIZED_OUTPUT_DISABLE) if @synchronized_output_depth.zero?
|
|
47
69
|
end
|
|
48
70
|
end
|
|
49
71
|
|
|
@@ -58,27 +80,28 @@ module Kward
|
|
|
58
80
|
visible = select_editing_active? if @select_state
|
|
59
81
|
visible = git_composing? if @git_state
|
|
60
82
|
visible = project_browser_search_active? if project_browser_visible?
|
|
83
|
+
visible = false if image_viewer_active?
|
|
61
84
|
set_cursor_visible_locked(visible)
|
|
62
85
|
end
|
|
63
86
|
|
|
64
87
|
def set_cursor_visible_locked(visible, force: false)
|
|
65
88
|
return if !force && @cursor_visible == visible
|
|
66
89
|
|
|
67
|
-
|
|
90
|
+
print_output_locked(visible ? CURSOR_SHOW : CURSOR_HIDE)
|
|
68
91
|
@cursor_visible = visible
|
|
69
92
|
end
|
|
70
93
|
|
|
71
94
|
def set_editor_bar_cursor_locked
|
|
72
95
|
return if @editor_bar_cursor_active
|
|
73
96
|
|
|
74
|
-
|
|
97
|
+
print_output_locked(CURSOR_SHAPE_BAR)
|
|
75
98
|
@editor_bar_cursor_active = true
|
|
76
99
|
end
|
|
77
100
|
|
|
78
|
-
def restore_editor_cursor_shape_locked
|
|
79
|
-
return unless @editor_bar_cursor_active
|
|
101
|
+
def restore_editor_cursor_shape_locked(force: false)
|
|
102
|
+
return unless force || @editor_bar_cursor_active
|
|
80
103
|
|
|
81
|
-
|
|
104
|
+
print_output_locked(CURSOR_SHAPE_DEFAULT)
|
|
82
105
|
@editor_bar_cursor_active = false
|
|
83
106
|
end
|
|
84
107
|
|
|
@@ -87,11 +110,6 @@ module Kward
|
|
|
87
110
|
ensure_scroll_region_locked(rows.length, width: width, height: height)
|
|
88
111
|
end
|
|
89
112
|
|
|
90
|
-
def make_room_for_composer_after_handoff_locked
|
|
91
|
-
rows, = composer_layout(screen_width, screen_height)
|
|
92
|
-
@output_io.print("\r\n" * rows.length)
|
|
93
|
-
end
|
|
94
|
-
|
|
95
113
|
def ensure_scroll_region_locked(row_count, redraw_transcript: true, width: screen_width, height: screen_height)
|
|
96
114
|
new_reserved_rows = [[row_count, 1].max, [height - 1, 1].max].min
|
|
97
115
|
return if @reserved_rows == new_reserved_rows && @last_height == height
|
|
@@ -100,13 +118,15 @@ module Kward
|
|
|
100
118
|
old_top = [height - old_reserved_rows + 1, 1].max
|
|
101
119
|
@reserved_rows = new_reserved_rows
|
|
102
120
|
new_top = composer_top_row(height)
|
|
103
|
-
|
|
121
|
+
print_output_locked(TerminalSequences.scroll_region(1, transcript_bottom_row(height)))
|
|
104
122
|
clear_screen_rows_locked(old_top, new_top - 1) if new_top > old_top
|
|
105
123
|
@last_composer_rows = []
|
|
106
124
|
redraw_transcript_locked(width: width, height: height) if redraw_transcript && new_reserved_rows < old_reserved_rows
|
|
107
125
|
end
|
|
108
126
|
|
|
109
127
|
def handle_resize_locked
|
|
128
|
+
return false if terminal_owned_by_child_locked?
|
|
129
|
+
|
|
110
130
|
current_width, current_height = screen_size
|
|
111
131
|
return false if current_width == @last_width && current_height == @last_height
|
|
112
132
|
|
|
@@ -127,7 +147,7 @@ module Kward
|
|
|
127
147
|
end
|
|
128
148
|
|
|
129
149
|
def restore_scroll_region_locked
|
|
130
|
-
|
|
150
|
+
print_output_locked(TerminalSequences.restore_scroll_region)
|
|
131
151
|
@reserved_rows = 0
|
|
132
152
|
end
|
|
133
153
|
|
|
@@ -143,15 +163,15 @@ module Kward
|
|
|
143
163
|
|
|
144
164
|
move_to_screen(top + index, 1)
|
|
145
165
|
if row
|
|
146
|
-
|
|
166
|
+
print_output_locked(row)
|
|
147
167
|
else
|
|
148
|
-
|
|
168
|
+
print_output_locked(TTY::Cursor.clear_line)
|
|
149
169
|
end
|
|
150
170
|
end
|
|
151
171
|
|
|
152
172
|
rows.length.upto(rows.length + rows_to_clear - 1) do |index|
|
|
153
173
|
move_to_screen(top + index, 1)
|
|
154
|
-
|
|
174
|
+
print_output_locked(TTY::Cursor.clear_line)
|
|
155
175
|
end
|
|
156
176
|
|
|
157
177
|
@last_composer_rows = rows.dup
|
|
@@ -191,12 +211,12 @@ module Kward
|
|
|
191
211
|
def clear_screen_rows_locked(top, bottom)
|
|
192
212
|
top.upto(bottom) do |row|
|
|
193
213
|
move_to_screen(row, 1)
|
|
194
|
-
|
|
214
|
+
print_output_locked(TTY::Cursor.clear_line)
|
|
195
215
|
end
|
|
196
216
|
end
|
|
197
217
|
|
|
198
218
|
def move_to_screen(row, col)
|
|
199
|
-
|
|
219
|
+
print_output_locked(TerminalSequences.move_to(row, col))
|
|
200
220
|
end
|
|
201
221
|
|
|
202
222
|
def screen_size
|
|
@@ -324,7 +324,7 @@ module Kward
|
|
|
324
324
|
@select_state[:selection_index] = result[:selection_index].to_i if result.key?(:selection_index)
|
|
325
325
|
end
|
|
326
326
|
render_prompt_locked
|
|
327
|
-
|
|
327
|
+
flush_output_locked
|
|
328
328
|
end
|
|
329
329
|
SELECT_CONTINUE
|
|
330
330
|
end
|
|
@@ -342,7 +342,7 @@ module Kward
|
|
|
342
342
|
@asking = true
|
|
343
343
|
reset_spinner_locked
|
|
344
344
|
render_prompt_locked
|
|
345
|
-
|
|
345
|
+
flush_output_locked
|
|
346
346
|
end
|
|
347
347
|
end
|
|
348
348
|
|
|
@@ -352,7 +352,7 @@ module Kward
|
|
|
352
352
|
@busy_activity = "streaming"
|
|
353
353
|
@select_state&.delete(:busy_activity)
|
|
354
354
|
render_prompt_locked if @asking
|
|
355
|
-
|
|
355
|
+
flush_output_locked
|
|
356
356
|
end
|
|
357
357
|
end
|
|
358
358
|
|
|
@@ -362,7 +362,7 @@ module Kward
|
|
|
362
362
|
spun = tick_spinner_locked
|
|
363
363
|
footer_refreshed = tick_footer_locked
|
|
364
364
|
render_prompt_locked if resized || spun || footer_refreshed
|
|
365
|
-
|
|
365
|
+
flush_output_locked if resized || spun || footer_refreshed
|
|
366
366
|
end
|
|
367
367
|
end
|
|
368
368
|
|
|
@@ -656,7 +656,7 @@ module Kward
|
|
|
656
656
|
self.composer_cursor = 0
|
|
657
657
|
@asking = true
|
|
658
658
|
render_prompt_locked if render
|
|
659
|
-
|
|
659
|
+
flush_output_locked
|
|
660
660
|
end
|
|
661
661
|
end
|
|
662
662
|
|
|
@@ -19,7 +19,7 @@ module Kward
|
|
|
19
19
|
@stream_state.finish_block if finish
|
|
20
20
|
restore_composer_cursor_locked unless @restoring_transcript
|
|
21
21
|
end
|
|
22
|
-
|
|
22
|
+
flush_output_locked unless @restoring_transcript
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
def write_transcript_text_locked(text)
|
|
@@ -32,7 +32,7 @@ module Kward
|
|
|
32
32
|
width, height = screen_size
|
|
33
33
|
output_text = terminal_newlines(text.to_s)
|
|
34
34
|
advance_pending_stream_wrap_locked(output_text, width: width, height: height)
|
|
35
|
-
|
|
35
|
+
print_output_locked(output_text)
|
|
36
36
|
update_stream_position(output_text, width: width)
|
|
37
37
|
end
|
|
38
38
|
|
|
@@ -58,7 +58,7 @@ module Kward
|
|
|
58
58
|
return if rows.empty?
|
|
59
59
|
|
|
60
60
|
move_to_screen(1, 1)
|
|
61
|
-
|
|
61
|
+
print_output_locked(terminal_newlines(rows.join("\n")))
|
|
62
62
|
end
|
|
63
63
|
|
|
64
64
|
def transcript_viewport_rows(row_count, width)
|
|
@@ -98,7 +98,7 @@ module Kward
|
|
|
98
98
|
return if output_text.empty? || output_text.start_with?("\r", "\n")
|
|
99
99
|
|
|
100
100
|
move_to_screen(transcript_bottom_row(height), width)
|
|
101
|
-
|
|
101
|
+
print_output_locked("\r\n")
|
|
102
102
|
@stream_state.clear_pending_wrap
|
|
103
103
|
end
|
|
104
104
|
|