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
|
@@ -4,6 +4,8 @@ require "pathname"
|
|
|
4
4
|
require "rbconfig"
|
|
5
5
|
require "thread"
|
|
6
6
|
require_relative "project_files"
|
|
7
|
+
require_relative "image_attachments"
|
|
8
|
+
require_relative "terminal_image_support"
|
|
7
9
|
require_relative "prompt_history"
|
|
8
10
|
require "tty-cursor"
|
|
9
11
|
require "tty-reader"
|
|
@@ -32,6 +34,7 @@ require_relative "prompt_interface/editor/renderer"
|
|
|
32
34
|
require_relative "prompt_interface/editor/syntax_highlighter"
|
|
33
35
|
require_relative "prompt_interface/editor/auto_close_pairs"
|
|
34
36
|
require_relative "prompt_interface/editor/endwise"
|
|
37
|
+
require_relative "prompt_interface/editor/word_completion"
|
|
35
38
|
require_relative "prompt_interface/editor/auto_indent"
|
|
36
39
|
require_relative "prompt_interface/composer_renderer"
|
|
37
40
|
require_relative "prompt_interface/composer_controller"
|
|
@@ -68,6 +71,7 @@ module Kward
|
|
|
68
71
|
FOOTER_REFRESH_INTERVAL = 1.0
|
|
69
72
|
COMPOSER_STATUS_REFRESH_INTERVAL = 1.0
|
|
70
73
|
COMPOSER_MAX_INPUT_ROWS = 6
|
|
74
|
+
IMAGE_VIEWER_MAX_ROWS = 8
|
|
71
75
|
TRANSCRIPT_BUFFER_LIMIT = 200_000
|
|
72
76
|
BANNER_MESSAGE = Banner::MESSAGE
|
|
73
77
|
|
|
@@ -83,6 +87,7 @@ module Kward
|
|
|
83
87
|
include EditorSyntaxHighlighter
|
|
84
88
|
include EditorAutoClosePairs
|
|
85
89
|
include EditorEndwise
|
|
90
|
+
include EditorWordCompletion
|
|
86
91
|
include EditorAutoIndent
|
|
87
92
|
include ComposerRenderer
|
|
88
93
|
include ComposerController
|
|
@@ -131,6 +136,9 @@ module Kward
|
|
|
131
136
|
def initialize(input: $stdin, output: $stdout, slash_commands: [], overlay_settings: nil, project_browser_icon_theme: "off", footer: nil, composer_status: nil, busy_help: true, attachment_badges: nil, attachment_parser: nil, banner_message: nil, tab_keybindings: nil, prompt_history: nil, workspace_root: Dir.pwd, editor_mode: nil, editor_mode_source: nil, editor_auto_indent: true, editor_auto_indent_source: nil, editor_auto_close_pairs: true, editor_auto_close_pairs_source: nil, editor_soft_wrap: true, editor_soft_wrap_source: nil, editor_bar_cursor: true, editor_bar_cursor_source: nil, editor_line_numbers: "absolute", editor_line_numbers_source: nil, diff_view: "auto", diff_view_source: nil, redraw_handler: nil)
|
|
132
137
|
@input_io = input
|
|
133
138
|
@output_io = output
|
|
139
|
+
# Logical state may change during a child handoff, but only the owner may render.
|
|
140
|
+
@terminal_owner = :kward
|
|
141
|
+
@terminal_handoff_mode = nil
|
|
134
142
|
@reader = TTY::Reader.new(input: input, output: output, interrupt: :error)
|
|
135
143
|
@mutex = Mutex.new
|
|
136
144
|
@prompt_history = prompt_history
|
|
@@ -177,9 +185,15 @@ module Kward
|
|
|
177
185
|
@file_open_dismissed_token = nil
|
|
178
186
|
@file_editor_open_status = nil
|
|
179
187
|
@file_mention_paths = nil
|
|
188
|
+
@project_browser_file_paths = nil
|
|
180
189
|
@project_browser_state = nil
|
|
190
|
+
@image_viewer_state = nil
|
|
191
|
+
@terminal_image_protocol = nil
|
|
181
192
|
@project_browser_restore_after_editor = false
|
|
182
193
|
@editor_state = nil
|
|
194
|
+
@editor_agent_suspended = false
|
|
195
|
+
@editor_prompt_active = false
|
|
196
|
+
@editor_prompt_input = ""
|
|
183
197
|
@interactive_state = nil
|
|
184
198
|
@last_interactive_tick = monotonic_now
|
|
185
199
|
@select_state = nil
|
|
@@ -237,8 +251,8 @@ module Kward
|
|
|
237
251
|
@started = true
|
|
238
252
|
@asking = true
|
|
239
253
|
disable_editor_mouse_reporting(force: true) unless editor_active?
|
|
240
|
-
|
|
241
|
-
|
|
254
|
+
print_output_locked(KEYBOARD_PROTOCOL_ENABLE)
|
|
255
|
+
print_output_locked(BRACKETED_PASTE_ENABLE)
|
|
242
256
|
render_prompt_locked if render
|
|
243
257
|
end
|
|
244
258
|
end
|
|
@@ -247,15 +261,16 @@ module Kward
|
|
|
247
261
|
@mutex.synchronize do
|
|
248
262
|
return unless @started
|
|
249
263
|
|
|
264
|
+
clear_image_viewer_output_locked if image_viewer_active?
|
|
250
265
|
clear_prompt_for_output_locked
|
|
251
266
|
restore_scroll_region_locked
|
|
252
267
|
disable_editor_mouse_reporting(force: true)
|
|
253
|
-
|
|
254
|
-
|
|
268
|
+
print_output_locked(BRACKETED_PASTE_RESTORE)
|
|
269
|
+
print_output_locked(KEYBOARD_PROTOCOL_RESTORE)
|
|
255
270
|
restore_editor_cursor_shape_locked
|
|
256
271
|
set_cursor_visible_locked(true, force: true)
|
|
257
|
-
|
|
258
|
-
|
|
272
|
+
puts_output_locked
|
|
273
|
+
flush_output_locked
|
|
259
274
|
@started = false
|
|
260
275
|
restore_console_mode_locked
|
|
261
276
|
end
|
|
@@ -281,14 +296,14 @@ module Kward
|
|
|
281
296
|
@stream_state.finish_block
|
|
282
297
|
render_prompt_after_output_locked
|
|
283
298
|
end
|
|
284
|
-
|
|
299
|
+
flush_output_locked
|
|
285
300
|
end
|
|
286
301
|
end
|
|
287
302
|
|
|
288
303
|
def restore_transcript
|
|
289
304
|
start(render: false) unless @started
|
|
290
305
|
@mutex.synchronize do
|
|
291
|
-
|
|
306
|
+
print_output_locked(SYNCHRONIZED_OUTPUT_ENABLE)
|
|
292
307
|
clear_prompt_for_output_locked unless @rendered_rows.zero? && @last_composer_rows.empty?
|
|
293
308
|
@transcript_buffer.clear
|
|
294
309
|
@transcript_viewport_rows = 0
|
|
@@ -303,8 +318,8 @@ module Kward
|
|
|
303
318
|
@restoring_transcript = false
|
|
304
319
|
width, height = screen_size
|
|
305
320
|
redraw_screen_locked(width: width, height: height)
|
|
306
|
-
|
|
307
|
-
|
|
321
|
+
print_output_locked(SYNCHRONIZED_OUTPUT_DISABLE)
|
|
322
|
+
flush_output_locked
|
|
308
323
|
end
|
|
309
324
|
end
|
|
310
325
|
|
|
@@ -343,6 +358,64 @@ module Kward
|
|
|
343
358
|
@mutex.synchronize { editor_active? }
|
|
344
359
|
end
|
|
345
360
|
|
|
361
|
+
# Returns the active Vibe or Modern editor buffer for an agent prompt turn.
|
|
362
|
+
def editor_prompt_context
|
|
363
|
+
@mutex.synchronize do
|
|
364
|
+
next nil unless editor_active? && (@editor_state.vibe? || @editor_state.modern?)
|
|
365
|
+
|
|
366
|
+
{
|
|
367
|
+
path: @editor_state.path,
|
|
368
|
+
display_path: @editor_state.display_path,
|
|
369
|
+
workspace_root: @workspace_root,
|
|
370
|
+
language: @editor_state.language,
|
|
371
|
+
content: @editor_state.buffer.dup,
|
|
372
|
+
cursor: @editor_state.cursor_line_and_column,
|
|
373
|
+
selection: @editor_state.selected_text
|
|
374
|
+
}
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
# Suspends the live editor while an agent prompt is running.
|
|
379
|
+
def suspend_editor_for_agent
|
|
380
|
+
@mutex.synchronize do
|
|
381
|
+
return false unless editor_active? && !@editor_agent_suspended
|
|
382
|
+
|
|
383
|
+
@editor_agent_suspended = true
|
|
384
|
+
@editor_state.status = "Agent is working · Ctrl+C cancels"
|
|
385
|
+
render_prompt_locked if @started
|
|
386
|
+
true
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
# Commits an agent draft as one undoable editor operation.
|
|
391
|
+
def commit_editor_prompt(session)
|
|
392
|
+
@mutex.synchronize do
|
|
393
|
+
return false unless editor_active? && @editor_agent_suspended
|
|
394
|
+
return false unless @editor_state.buffer == session.initial_content
|
|
395
|
+
|
|
396
|
+
changed = @editor_state.replace_buffer(session.content)
|
|
397
|
+
@editor_state.status = changed ? "Agent updated buffer · :w save" : "Agent made no buffer changes"
|
|
398
|
+
render_prompt_locked if @started
|
|
399
|
+
changed
|
|
400
|
+
end
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
# Resumes the editor after an agent prompt turn.
|
|
404
|
+
def resume_editor_for_agent(status: nil)
|
|
405
|
+
@mutex.synchronize do
|
|
406
|
+
return false unless editor_active?
|
|
407
|
+
|
|
408
|
+
@editor_agent_suspended = false
|
|
409
|
+
@editor_state.status = status.to_s unless status.to_s.empty?
|
|
410
|
+
render_prompt_locked if @started
|
|
411
|
+
true
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
def inline_image_protocol
|
|
416
|
+
@mutex.synchronize { terminal_image_protocol }
|
|
417
|
+
end
|
|
418
|
+
|
|
346
419
|
def update_workspace_root(root, prompt_history: nil)
|
|
347
420
|
@mutex.synchronize do
|
|
348
421
|
expanded_root = File.expand_path(root.to_s.empty? ? Dir.pwd : root)
|
|
@@ -467,6 +540,21 @@ module Kward
|
|
|
467
540
|
perform_pending_transcript_redraw
|
|
468
541
|
end
|
|
469
542
|
|
|
543
|
+
# Prefills the next composer input without submitting it.
|
|
544
|
+
def prefill_input(value)
|
|
545
|
+
@mutex.synchronize do
|
|
546
|
+
@composer.prefill_input = value.to_s
|
|
547
|
+
end
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
# Updates the visible prompt label while the composer is active.
|
|
551
|
+
def update_prompt_label(label)
|
|
552
|
+
@mutex.synchronize do
|
|
553
|
+
@prompt_label = label.to_s
|
|
554
|
+
render_prompt_locked if @started && @asking
|
|
555
|
+
end
|
|
556
|
+
end
|
|
557
|
+
|
|
470
558
|
def yes?(message, default: false)
|
|
471
559
|
answer = ask("#{message} #{default ? "[Y/n]" : "[y/N]"}")
|
|
472
560
|
return default if answer.nil?
|
|
@@ -575,16 +663,21 @@ module Kward
|
|
|
575
663
|
@interactive_state = nil
|
|
576
664
|
restore_composer_snapshot_locked(snapshot)
|
|
577
665
|
redraw_screen_locked if @started
|
|
578
|
-
|
|
666
|
+
flush_output_locked
|
|
579
667
|
end
|
|
580
668
|
end
|
|
581
669
|
|
|
582
|
-
def
|
|
670
|
+
def with_inline_terminal_handoff(&block)
|
|
671
|
+
with_terminal_handoff(inline: true, &block)
|
|
672
|
+
end
|
|
673
|
+
|
|
674
|
+
def with_terminal_handoff(inline: false)
|
|
583
675
|
start
|
|
584
676
|
input = nil
|
|
585
677
|
output = nil
|
|
678
|
+
transition = nil
|
|
586
679
|
@mutex.synchronize do
|
|
587
|
-
if
|
|
680
|
+
if inline
|
|
588
681
|
handle_resize_locked
|
|
589
682
|
reserve_composer_region_locked
|
|
590
683
|
move_to_transcript_cursor_locked
|
|
@@ -593,27 +686,36 @@ module Kward
|
|
|
593
686
|
restore_scroll_region_locked
|
|
594
687
|
end
|
|
595
688
|
disable_editor_mouse_reporting(force: true)
|
|
596
|
-
|
|
597
|
-
|
|
689
|
+
print_output_locked(BRACKETED_PASTE_RESTORE)
|
|
690
|
+
print_output_locked(KEYBOARD_PROTOCOL_RESTORE)
|
|
598
691
|
restore_editor_cursor_shape_locked
|
|
599
692
|
set_cursor_visible_locked(true, force: true)
|
|
600
|
-
|
|
693
|
+
flush_output_locked
|
|
601
694
|
restore_console_mode_locked
|
|
602
695
|
input = @input_io
|
|
603
696
|
output = @output_io
|
|
697
|
+
transition = method(:switch_terminal_handoff_to_exclusive)
|
|
698
|
+
@terminal_handoff_mode = inline ? :inline : :exclusive
|
|
699
|
+
@terminal_owner = :child
|
|
604
700
|
end
|
|
605
701
|
|
|
606
|
-
yield(input, output)
|
|
702
|
+
yield(input, output, transition)
|
|
607
703
|
ensure
|
|
608
704
|
@mutex.synchronize do
|
|
609
|
-
|
|
610
|
-
|
|
705
|
+
@terminal_owner = :kward
|
|
706
|
+
@terminal_handoff_mode = nil
|
|
707
|
+
restore_scroll_region_locked
|
|
708
|
+
print_output_locked(TerminalSequences::SGR_RESET)
|
|
611
709
|
enter_raw_mode_locked
|
|
612
|
-
|
|
613
|
-
|
|
710
|
+
print_output_locked(KEYBOARD_PROTOCOL_ENABLE)
|
|
711
|
+
print_output_locked(BRACKETED_PASTE_ENABLE)
|
|
712
|
+
disable_editor_mouse_reporting(force: true)
|
|
713
|
+
restore_editor_cursor_shape_locked(force: true)
|
|
714
|
+
@cursor_visible = nil
|
|
614
715
|
@last_composer_rows = []
|
|
615
|
-
|
|
616
|
-
|
|
716
|
+
width, height = screen_size
|
|
717
|
+
with_synchronized_output_locked { redraw_screen_locked(width: width, height: height) }
|
|
718
|
+
flush_output_locked
|
|
617
719
|
end
|
|
618
720
|
end
|
|
619
721
|
|
|
@@ -851,7 +953,7 @@ module Kward
|
|
|
851
953
|
@stream_state.finish_block
|
|
852
954
|
restore_composer_cursor_locked
|
|
853
955
|
end
|
|
854
|
-
|
|
956
|
+
flush_output_locked
|
|
855
957
|
end
|
|
856
958
|
end
|
|
857
959
|
|
|
@@ -873,7 +975,7 @@ module Kward
|
|
|
873
975
|
end
|
|
874
976
|
end
|
|
875
977
|
|
|
876
|
-
def record_transient_terminal_output(text)
|
|
978
|
+
def record_transient_terminal_output(text, render: true)
|
|
877
979
|
value = text.to_s
|
|
878
980
|
return if value.empty?
|
|
879
981
|
|
|
@@ -883,8 +985,10 @@ module Kward
|
|
|
883
985
|
@stream_state.finish_block
|
|
884
986
|
width, height = screen_size
|
|
885
987
|
remember_transcript_viewport_locked(height)
|
|
988
|
+
next unless render
|
|
989
|
+
|
|
886
990
|
with_synchronized_output_locked { redraw_screen_locked(width: width, height: height) }
|
|
887
|
-
|
|
991
|
+
flush_output_locked
|
|
888
992
|
end
|
|
889
993
|
end
|
|
890
994
|
|
|
@@ -895,7 +999,7 @@ module Kward
|
|
|
895
999
|
write_transcript_text_locked(delta.to_s)
|
|
896
1000
|
restore_composer_cursor_locked unless @restoring_transcript
|
|
897
1001
|
end
|
|
898
|
-
|
|
1002
|
+
flush_output_locked unless @restoring_transcript
|
|
899
1003
|
end
|
|
900
1004
|
end
|
|
901
1005
|
|
|
@@ -909,7 +1013,7 @@ module Kward
|
|
|
909
1013
|
@mutex.synchronize do
|
|
910
1014
|
width, height = screen_size
|
|
911
1015
|
with_synchronized_output_locked { redraw_screen_locked(width: width, height: height) }
|
|
912
|
-
|
|
1016
|
+
flush_output_locked
|
|
913
1017
|
end
|
|
914
1018
|
end
|
|
915
1019
|
|
|
@@ -931,12 +1035,27 @@ module Kward
|
|
|
931
1035
|
@stream_state.reset
|
|
932
1036
|
width, height = screen_size
|
|
933
1037
|
with_synchronized_output_locked { redraw_screen_locked(width: width, height: height) }
|
|
934
|
-
|
|
1038
|
+
flush_output_locked
|
|
935
1039
|
end
|
|
936
1040
|
end
|
|
937
1041
|
|
|
938
1042
|
private
|
|
939
1043
|
|
|
1044
|
+
def switch_terminal_handoff_to_exclusive
|
|
1045
|
+
@mutex.synchronize do
|
|
1046
|
+
return unless @terminal_owner == :child && @terminal_handoff_mode == :inline
|
|
1047
|
+
|
|
1048
|
+
@terminal_owner = :kward
|
|
1049
|
+
with_synchronized_output_locked do
|
|
1050
|
+
clear_prompt_for_output_locked
|
|
1051
|
+
restore_scroll_region_locked
|
|
1052
|
+
end
|
|
1053
|
+
flush_output_locked
|
|
1054
|
+
@terminal_handoff_mode = :exclusive
|
|
1055
|
+
@terminal_owner = :child
|
|
1056
|
+
end
|
|
1057
|
+
end
|
|
1058
|
+
|
|
940
1059
|
def write_transcript_message(message, preserve_composer:)
|
|
941
1060
|
@mutex.synchronize do
|
|
942
1061
|
text = message.to_s
|
|
@@ -958,12 +1077,11 @@ module Kward
|
|
|
958
1077
|
@stream_state.finish_block
|
|
959
1078
|
preserve_composer ? restore_composer_cursor_locked : render_prompt_after_output_locked
|
|
960
1079
|
end
|
|
961
|
-
|
|
1080
|
+
flush_output_locked
|
|
962
1081
|
end
|
|
963
1082
|
end
|
|
964
1083
|
|
|
965
1084
|
def request_transcript_redraw
|
|
966
|
-
redraw_screen_locked
|
|
967
1085
|
@transcript_redraw_requested = true
|
|
968
1086
|
end
|
|
969
1087
|
|
|
@@ -973,7 +1091,13 @@ module Kward
|
|
|
973
1091
|
@transcript_redraw_requested = false
|
|
974
1092
|
pending
|
|
975
1093
|
end
|
|
976
|
-
|
|
1094
|
+
return unless requested
|
|
1095
|
+
|
|
1096
|
+
if @redraw_handler
|
|
1097
|
+
@redraw_handler.call
|
|
1098
|
+
else
|
|
1099
|
+
redraw
|
|
1100
|
+
end
|
|
977
1101
|
end
|
|
978
1102
|
|
|
979
1103
|
def prompt_workspace_root
|
|
@@ -982,9 +1106,12 @@ module Kward
|
|
|
982
1106
|
|
|
983
1107
|
def reset_workspace_file_state_locked
|
|
984
1108
|
@file_mention_paths = nil
|
|
1109
|
+
@project_browser_file_paths = nil
|
|
985
1110
|
@file_mention_path_entries_paths = nil
|
|
986
1111
|
@file_mention_path_entries = nil
|
|
987
1112
|
@project_browser_state = nil
|
|
1113
|
+
@image_viewer_state = nil
|
|
1114
|
+
@terminal_image_protocol = nil
|
|
988
1115
|
@project_browser_tree_paths = nil
|
|
989
1116
|
@project_browser_tree = nil
|
|
990
1117
|
@file_overlay_dismissed_token = nil
|
|
@@ -32,6 +32,7 @@ module Kward
|
|
|
32
32
|
{ name: "scratchpad", description: "Open an unsaved editor buffer.", argument_hint: "[text|markdown|ruby]" },
|
|
33
33
|
{ name: "pty", description: "Run a command in an interactive PTY passthrough session.", argument_hint: "<command>" },
|
|
34
34
|
{ name: "tab", description: "Manage tabs.", argument_hint: "[1-n|move|close|new|name|worktree]" },
|
|
35
|
+
{ name: "worktree", description: "Manage the active tab's linked Git worktree.", argument_hint: "[activate|detach|status|merge|merge abort|remove]" },
|
|
35
36
|
{ name: "status", description: "Show the current status message.", argument_hint: "" },
|
|
36
37
|
{ name: "stats", description: "Show telemetry logging stats.", argument_hint: "[range]" },
|
|
37
38
|
{ name: "sandbox", description: "Inspect or configure model command sandboxing.", argument_hint: "[status|off|read_only|workspace_write|network allow|network deny]" },
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Namespace for the Kward CLI agent runtime.
|
|
2
|
+
module Kward
|
|
3
|
+
# Forwards PTY output immediately and optionally keeps a bounded byte copy.
|
|
4
|
+
#
|
|
5
|
+
# The runner only depends on the sink's `write` method and optional `flush`
|
|
6
|
+
# method. Capture is deliberately byte-oriented so PTY chunk boundaries have
|
|
7
|
+
# no effect on the retained output.
|
|
8
|
+
class PassthroughPtyOutputSink
|
|
9
|
+
attr_reader :captured_output
|
|
10
|
+
|
|
11
|
+
def initialize(output:, max_capture_bytes: nil)
|
|
12
|
+
@output = output
|
|
13
|
+
@max_capture_bytes = max_capture_bytes
|
|
14
|
+
@captured_output = max_capture_bytes ? +"".b : nil
|
|
15
|
+
@truncated = false
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def write(chunk)
|
|
19
|
+
@output.write(chunk)
|
|
20
|
+
capture(chunk)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def flush
|
|
24
|
+
@output.flush if @output.respond_to?(:flush)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def truncated?
|
|
28
|
+
@truncated
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def capture(chunk)
|
|
34
|
+
return unless @captured_output
|
|
35
|
+
|
|
36
|
+
remaining = @max_capture_bytes - @captured_output.bytesize
|
|
37
|
+
if chunk.bytesize > remaining
|
|
38
|
+
@captured_output << chunk.byteslice(0, remaining) if remaining.positive?
|
|
39
|
+
@truncated = true
|
|
40
|
+
else
|
|
41
|
+
@captured_output << chunk
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
require_relative "ansi"
|
|
2
|
+
|
|
3
|
+
# Namespace for the Kward CLI agent runtime.
|
|
4
|
+
module Kward
|
|
5
|
+
# Reduces safe, line-oriented PTY redraws to transcript-friendly text.
|
|
6
|
+
class PtyTranscriptNormalizer
|
|
7
|
+
HORIZONTAL_REDRAW_PATTERN = /\r|\e\[[0-9;]*[CDGK`]/.freeze
|
|
8
|
+
|
|
9
|
+
def self.normalize(text)
|
|
10
|
+
text.split("\n", -1).map do |line|
|
|
11
|
+
if line.match?(HORIZONTAL_REDRAW_PATTERN)
|
|
12
|
+
Line.new(line).render
|
|
13
|
+
else
|
|
14
|
+
ANSI.sanitize_transcript(line)
|
|
15
|
+
end
|
|
16
|
+
end.join("\n")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Applies the horizontal cursor controls accepted by AdaptivePtyOutputSink
|
|
20
|
+
# without modeling a complete terminal screen.
|
|
21
|
+
class Line
|
|
22
|
+
def initialize(text)
|
|
23
|
+
@text = text
|
|
24
|
+
@cells = []
|
|
25
|
+
@cursor = 0
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def render
|
|
29
|
+
ANSI.scan_escape_tokens(@text).each do |token|
|
|
30
|
+
token[:escape] ? apply_escape(token[:text]) : write_text(token[:text])
|
|
31
|
+
end
|
|
32
|
+
@cells.join.rstrip
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def apply_escape(sequence)
|
|
38
|
+
final = sequence[-1]
|
|
39
|
+
return set_column(sequence) if final == "G" || final == "`"
|
|
40
|
+
return move_cursor(sequence, 1) if final == "C"
|
|
41
|
+
return move_cursor(sequence, -1) if final == "D"
|
|
42
|
+
return erase_line(sequence) if final == "K"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def set_column(sequence)
|
|
46
|
+
@cursor = [parameter(sequence, default: 1), 1].max - 1
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def move_cursor(sequence, direction)
|
|
50
|
+
distance = [parameter(sequence, default: 1), 1].max
|
|
51
|
+
@cursor = [@cursor + (distance * direction), 0].max
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def erase_line(sequence)
|
|
55
|
+
case parameter(sequence, default: 0)
|
|
56
|
+
when 1
|
|
57
|
+
fill_to_cursor
|
|
58
|
+
0.upto(@cursor) { |index| @cells[index] = " " }
|
|
59
|
+
when 2
|
|
60
|
+
@cells.clear
|
|
61
|
+
else
|
|
62
|
+
@cells.slice!(@cursor..) if @cursor < @cells.length
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def parameter(sequence, default:)
|
|
67
|
+
value = sequence[2...-1].to_s.split(";", 2).first.to_s
|
|
68
|
+
value.empty? ? default : value.to_i
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def write_text(text)
|
|
72
|
+
text.each_char do |character|
|
|
73
|
+
case character
|
|
74
|
+
when "\r"
|
|
75
|
+
@cursor = 0
|
|
76
|
+
when "\b"
|
|
77
|
+
@cursor = [@cursor - 1, 0].max
|
|
78
|
+
when "\t"
|
|
79
|
+
@cursor = ((@cursor / 8) + 1) * 8
|
|
80
|
+
else
|
|
81
|
+
fill_to_cursor
|
|
82
|
+
@cells[@cursor] = character
|
|
83
|
+
@cursor += 1
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def fill_to_cursor
|
|
89
|
+
@cells.concat([" "] * (@cursor - @cells.length)) if @cursor > @cells.length
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -235,7 +235,7 @@ module Kward
|
|
|
235
235
|
def complete_login(login, code)
|
|
236
236
|
raise "Missing authorization code" if code.to_s.empty?
|
|
237
237
|
|
|
238
|
-
login.oauth.complete_login_flow(code: code, redirect_uri: login.redirect_uri, code_verifier: login.pkce[:verifier])
|
|
238
|
+
login.oauth.complete_login_flow(code: code, redirect_uri: login.redirect_uri, code_verifier: login.pkce[:verifier], state: login.state)
|
|
239
239
|
login.status = "completed"
|
|
240
240
|
@server.notify("auth/loginFinished", login_payload(login))
|
|
241
241
|
rescue StandardError => e
|
data/lib/kward/rpc/server.rb
CHANGED
|
@@ -610,7 +610,8 @@ module Kward
|
|
|
610
610
|
},
|
|
611
611
|
composer: {
|
|
612
612
|
sessionDiff: { supported: false, reason: "interactiveComposerOnly" },
|
|
613
|
-
copy: { supported: false, reason: "clientClipboardOwnedByUi" }
|
|
613
|
+
copy: { supported: false, reason: "clientClipboardOwnedByUi" },
|
|
614
|
+
editorPrompt: { supported: false, reason: "interactiveVibeEditorOnly" }
|
|
614
615
|
},
|
|
615
616
|
security: {
|
|
616
617
|
workspaceMutationGuard: "none",
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require_relative "private_file"
|
|
3
|
+
|
|
4
|
+
# Namespace for the Kward CLI agent runtime.
|
|
5
|
+
module Kward
|
|
6
|
+
# Rebuildable lightweight summaries for persisted session event logs.
|
|
7
|
+
#
|
|
8
|
+
# Session JSONL files remain authoritative. Catalog entries are accepted only
|
|
9
|
+
# while their source fingerprint matches and may be deleted at any time.
|
|
10
|
+
class SessionCatalog
|
|
11
|
+
VERSION = 1
|
|
12
|
+
INDEX_DIRECTORY = ".index"
|
|
13
|
+
FILENAME = "sessions.json"
|
|
14
|
+
|
|
15
|
+
def initialize(session_dir:)
|
|
16
|
+
@path = File.join(session_dir, INDEX_DIRECTORY, FILENAME)
|
|
17
|
+
@entries = load_entries
|
|
18
|
+
@dirty = false
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def fetch(path)
|
|
22
|
+
entry = @entries[entry_key(path)]
|
|
23
|
+
return nil unless entry.is_a?(Hash)
|
|
24
|
+
return nil unless entry["source"] == fingerprint(path)
|
|
25
|
+
|
|
26
|
+
entry["summary"] if entry["summary"].is_a?(Hash)
|
|
27
|
+
rescue StandardError
|
|
28
|
+
nil
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def write(path, summary, fingerprint: self.fingerprint(path))
|
|
32
|
+
@entries[entry_key(path)] = {
|
|
33
|
+
"source" => fingerprint,
|
|
34
|
+
"summary" => summary
|
|
35
|
+
}
|
|
36
|
+
@dirty = true
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def remove(path)
|
|
40
|
+
@dirty = true if @entries.delete(entry_key(path))
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def retain(paths)
|
|
44
|
+
keys = paths.to_h { |path| [entry_key(path), true] }
|
|
45
|
+
previous_size = @entries.size
|
|
46
|
+
@entries.delete_if { |key, _entry| !keys[key] }
|
|
47
|
+
@dirty = true if @entries.size != previous_size
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def flush
|
|
51
|
+
return unless @dirty
|
|
52
|
+
|
|
53
|
+
PrivateFile.write_json(@path, {
|
|
54
|
+
"version" => VERSION,
|
|
55
|
+
"entries" => @entries
|
|
56
|
+
})
|
|
57
|
+
@dirty = false
|
|
58
|
+
rescue StandardError
|
|
59
|
+
nil
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def fingerprint(path)
|
|
63
|
+
stat = File.stat(path)
|
|
64
|
+
{
|
|
65
|
+
"size" => stat.size,
|
|
66
|
+
"inode" => stat.ino,
|
|
67
|
+
"mtimeSeconds" => stat.mtime.to_i,
|
|
68
|
+
"mtimeNanoseconds" => stat.mtime.nsec
|
|
69
|
+
}
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def load_entries
|
|
75
|
+
data = JSON.parse(File.read(@path))
|
|
76
|
+
return {} unless data["version"] == VERSION && data["entries"].is_a?(Hash)
|
|
77
|
+
|
|
78
|
+
data["entries"]
|
|
79
|
+
rescue StandardError
|
|
80
|
+
{}
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def entry_key(path)
|
|
84
|
+
File.basename(path)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|