kward 0.80.1 → 0.82.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 +62 -0
- data/Gemfile.lock +2 -2
- data/README.md +2 -2
- data/Rakefile +44 -4
- data/doc/composer.md +6 -9
- data/doc/configuration.md +13 -3
- data/doc/files.md +8 -2
- data/doc/permissions.md +1 -1
- data/doc/releasing.md +71 -38
- data/doc/rpc.md +2 -1
- data/doc/sandboxing.md +4 -4
- data/doc/security.md +6 -9
- data/doc/shell.md +161 -196
- data/doc/skills.md +17 -5
- data/doc/tabs.md +1 -1
- data/doc/usage.md +6 -5
- data/kward.gemspec +1 -1
- data/lib/kward/adaptive_pty_output_sink.rb +183 -0
- data/lib/kward/ansi.rb +9 -1
- data/lib/kward/cli/commands.rb +7 -0
- data/lib/kward/cli/git.rb +5 -1
- data/lib/kward/cli/interactive_turn.rb +1 -1
- data/lib/kward/cli/plugins.rb +1 -1
- data/lib/kward/cli/project_skills.rb +99 -0
- data/lib/kward/cli/project_skills_commands.rb +87 -0
- data/lib/kward/cli/prompt_interface.rb +18 -4
- data/lib/kward/cli/rendering.rb +41 -3
- data/lib/kward/cli/runtime_helpers.rb +222 -21
- data/lib/kward/cli/sessions.rb +6 -4
- data/lib/kward/cli/settings.rb +5 -13
- data/lib/kward/cli/slash_commands.rb +6 -0
- data/lib/kward/cli/tabs.rb +62 -5
- data/lib/kward/cli.rb +21 -1
- data/lib/kward/config_files.rb +9 -4
- data/lib/kward/conversation.rb +8 -5
- data/lib/kward/ekwsh.rb +37 -16
- data/lib/kward/image_attachments.rb +98 -15
- data/lib/kward/interactive_pty_runner.rb +102 -30
- data/lib/kward/prompt_interface/composer_controller.rb +15 -3
- data/lib/kward/prompt_interface/composer_renderer.rb +27 -0
- data/lib/kward/prompt_interface/editor/controller.rb +10 -6
- data/lib/kward/prompt_interface/editor/modes/emacs.rb +2 -2
- data/lib/kward/prompt_interface/editor/modes/vibe.rb +2 -2
- data/lib/kward/prompt_interface/git_prompt.rb +1 -1
- data/lib/kward/prompt_interface/key_handler.rb +100 -43
- data/lib/kward/prompt_interface/overlay_renderer.rb +13 -0
- data/lib/kward/prompt_interface/project_browser.rb +162 -3
- 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/screen.rb +40 -15
- 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 +167 -44
- data/lib/kward/prompts/commands.rb +2 -0
- data/lib/kward/prompts.rb +6 -6
- data/lib/kward/pty_output_sink.rb +45 -0
- data/lib/kward/pty_transcript_normalizer.rb +93 -0
- data/lib/kward/rpc/server.rb +1 -0
- data/lib/kward/session_catalog.rb +87 -0
- data/lib/kward/session_store.rb +97 -7
- data/lib/kward/skills/registry.rb +52 -2
- data/lib/kward/skills/trust_coordinator.rb +45 -0
- data/lib/kward/skills/trust_store.rb +107 -0
- data/lib/kward/terminal_image_support.rb +116 -0
- data/lib/kward/terminal_sequences.rb +43 -0
- data/lib/kward/version.rb +1 -1
- metadata +11 -4
- data/.github/workflows/ci.yml +0 -48
- data/.github/workflows/pages.yml +0 -48
|
@@ -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
|
|
|
@@ -95,13 +118,15 @@ module Kward
|
|
|
95
118
|
old_top = [height - old_reserved_rows + 1, 1].max
|
|
96
119
|
@reserved_rows = new_reserved_rows
|
|
97
120
|
new_top = composer_top_row(height)
|
|
98
|
-
|
|
121
|
+
print_output_locked(TerminalSequences.scroll_region(1, transcript_bottom_row(height)))
|
|
99
122
|
clear_screen_rows_locked(old_top, new_top - 1) if new_top > old_top
|
|
100
123
|
@last_composer_rows = []
|
|
101
124
|
redraw_transcript_locked(width: width, height: height) if redraw_transcript && new_reserved_rows < old_reserved_rows
|
|
102
125
|
end
|
|
103
126
|
|
|
104
127
|
def handle_resize_locked
|
|
128
|
+
return false if terminal_owned_by_child_locked?
|
|
129
|
+
|
|
105
130
|
current_width, current_height = screen_size
|
|
106
131
|
return false if current_width == @last_width && current_height == @last_height
|
|
107
132
|
|
|
@@ -122,7 +147,7 @@ module Kward
|
|
|
122
147
|
end
|
|
123
148
|
|
|
124
149
|
def restore_scroll_region_locked
|
|
125
|
-
|
|
150
|
+
print_output_locked(TerminalSequences.restore_scroll_region)
|
|
126
151
|
@reserved_rows = 0
|
|
127
152
|
end
|
|
128
153
|
|
|
@@ -138,15 +163,15 @@ module Kward
|
|
|
138
163
|
|
|
139
164
|
move_to_screen(top + index, 1)
|
|
140
165
|
if row
|
|
141
|
-
|
|
166
|
+
print_output_locked(row)
|
|
142
167
|
else
|
|
143
|
-
|
|
168
|
+
print_output_locked(TTY::Cursor.clear_line)
|
|
144
169
|
end
|
|
145
170
|
end
|
|
146
171
|
|
|
147
172
|
rows.length.upto(rows.length + rows_to_clear - 1) do |index|
|
|
148
173
|
move_to_screen(top + index, 1)
|
|
149
|
-
|
|
174
|
+
print_output_locked(TTY::Cursor.clear_line)
|
|
150
175
|
end
|
|
151
176
|
|
|
152
177
|
@last_composer_rows = rows.dup
|
|
@@ -186,12 +211,12 @@ module Kward
|
|
|
186
211
|
def clear_screen_rows_locked(top, bottom)
|
|
187
212
|
top.upto(bottom) do |row|
|
|
188
213
|
move_to_screen(row, 1)
|
|
189
|
-
|
|
214
|
+
print_output_locked(TTY::Cursor.clear_line)
|
|
190
215
|
end
|
|
191
216
|
end
|
|
192
217
|
|
|
193
218
|
def move_to_screen(row, col)
|
|
194
|
-
|
|
219
|
+
print_output_locked(TerminalSequences.move_to(row, col))
|
|
195
220
|
end
|
|
196
221
|
|
|
197
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
|
|
|
@@ -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"
|
|
@@ -68,6 +70,7 @@ module Kward
|
|
|
68
70
|
FOOTER_REFRESH_INTERVAL = 1.0
|
|
69
71
|
COMPOSER_STATUS_REFRESH_INTERVAL = 1.0
|
|
70
72
|
COMPOSER_MAX_INPUT_ROWS = 6
|
|
73
|
+
IMAGE_VIEWER_MAX_ROWS = 8
|
|
71
74
|
TRANSCRIPT_BUFFER_LIMIT = 200_000
|
|
72
75
|
BANNER_MESSAGE = Banner::MESSAGE
|
|
73
76
|
|
|
@@ -128,9 +131,12 @@ module Kward
|
|
|
128
131
|
end
|
|
129
132
|
end
|
|
130
133
|
|
|
131
|
-
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)
|
|
134
|
+
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
135
|
@input_io = input
|
|
133
136
|
@output_io = output
|
|
137
|
+
# Logical state may change during a child handoff, but only the owner may render.
|
|
138
|
+
@terminal_owner = :kward
|
|
139
|
+
@terminal_handoff_mode = nil
|
|
134
140
|
@reader = TTY::Reader.new(input: input, output: output, interrupt: :error)
|
|
135
141
|
@mutex = Mutex.new
|
|
136
142
|
@prompt_history = prompt_history
|
|
@@ -162,6 +168,10 @@ module Kward
|
|
|
162
168
|
@restoring_transcript = false
|
|
163
169
|
@pending_keys = []
|
|
164
170
|
@completion_provider = nil
|
|
171
|
+
@completion_cycle = nil
|
|
172
|
+
@completion_overlay = nil
|
|
173
|
+
@redraw_handler = redraw_handler
|
|
174
|
+
@transcript_redraw_requested = false
|
|
165
175
|
@original_console_mode = nil
|
|
166
176
|
@raw_mode_active = false
|
|
167
177
|
@slash_commands = normalize_slash_commands(slash_commands)
|
|
@@ -174,6 +184,8 @@ module Kward
|
|
|
174
184
|
@file_editor_open_status = nil
|
|
175
185
|
@file_mention_paths = nil
|
|
176
186
|
@project_browser_state = nil
|
|
187
|
+
@image_viewer_state = nil
|
|
188
|
+
@terminal_image_protocol = nil
|
|
177
189
|
@project_browser_restore_after_editor = false
|
|
178
190
|
@editor_state = nil
|
|
179
191
|
@interactive_state = nil
|
|
@@ -233,8 +245,8 @@ module Kward
|
|
|
233
245
|
@started = true
|
|
234
246
|
@asking = true
|
|
235
247
|
disable_editor_mouse_reporting(force: true) unless editor_active?
|
|
236
|
-
|
|
237
|
-
|
|
248
|
+
print_output_locked(KEYBOARD_PROTOCOL_ENABLE)
|
|
249
|
+
print_output_locked(BRACKETED_PASTE_ENABLE)
|
|
238
250
|
render_prompt_locked if render
|
|
239
251
|
end
|
|
240
252
|
end
|
|
@@ -243,39 +255,27 @@ module Kward
|
|
|
243
255
|
@mutex.synchronize do
|
|
244
256
|
return unless @started
|
|
245
257
|
|
|
258
|
+
clear_image_viewer_output_locked if image_viewer_active?
|
|
246
259
|
clear_prompt_for_output_locked
|
|
247
260
|
restore_scroll_region_locked
|
|
248
261
|
disable_editor_mouse_reporting(force: true)
|
|
249
|
-
|
|
250
|
-
|
|
262
|
+
print_output_locked(BRACKETED_PASTE_RESTORE)
|
|
263
|
+
print_output_locked(KEYBOARD_PROTOCOL_RESTORE)
|
|
251
264
|
restore_editor_cursor_shape_locked
|
|
252
265
|
set_cursor_visible_locked(true, force: true)
|
|
253
|
-
|
|
254
|
-
|
|
266
|
+
puts_output_locked
|
|
267
|
+
flush_output_locked
|
|
255
268
|
@started = false
|
|
256
269
|
restore_console_mode_locked
|
|
257
270
|
end
|
|
258
271
|
end
|
|
259
272
|
|
|
260
273
|
def say(message)
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
if @restoring_transcript
|
|
264
|
-
write_transcript_text_locked(text)
|
|
265
|
-
write_transcript_text_locked("\n") unless text.end_with?("\n")
|
|
266
|
-
@stream_state.finish_block
|
|
267
|
-
next
|
|
268
|
-
end
|
|
274
|
+
write_transcript_message(message, preserve_composer: false)
|
|
275
|
+
end
|
|
269
276
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
write_transcript_text_locked(text)
|
|
273
|
-
write_transcript_text_locked("\n") unless text.end_with?("\n")
|
|
274
|
-
@stream_state.finish_block
|
|
275
|
-
render_prompt_after_output_locked
|
|
276
|
-
end
|
|
277
|
-
@output_io.flush
|
|
278
|
-
end
|
|
277
|
+
def write_transcript(message)
|
|
278
|
+
write_transcript_message(message, preserve_composer: true)
|
|
279
279
|
end
|
|
280
280
|
|
|
281
281
|
def say_visual(message)
|
|
@@ -290,14 +290,14 @@ module Kward
|
|
|
290
290
|
@stream_state.finish_block
|
|
291
291
|
render_prompt_after_output_locked
|
|
292
292
|
end
|
|
293
|
-
|
|
293
|
+
flush_output_locked
|
|
294
294
|
end
|
|
295
295
|
end
|
|
296
296
|
|
|
297
297
|
def restore_transcript
|
|
298
298
|
start(render: false) unless @started
|
|
299
299
|
@mutex.synchronize do
|
|
300
|
-
|
|
300
|
+
print_output_locked(SYNCHRONIZED_OUTPUT_ENABLE)
|
|
301
301
|
clear_prompt_for_output_locked unless @rendered_rows.zero? && @last_composer_rows.empty?
|
|
302
302
|
@transcript_buffer.clear
|
|
303
303
|
@transcript_viewport_rows = 0
|
|
@@ -312,8 +312,8 @@ module Kward
|
|
|
312
312
|
@restoring_transcript = false
|
|
313
313
|
width, height = screen_size
|
|
314
314
|
redraw_screen_locked(width: width, height: height)
|
|
315
|
-
|
|
316
|
-
|
|
315
|
+
print_output_locked(SYNCHRONIZED_OUTPUT_DISABLE)
|
|
316
|
+
flush_output_locked
|
|
317
317
|
end
|
|
318
318
|
end
|
|
319
319
|
|
|
@@ -328,6 +328,16 @@ module Kward
|
|
|
328
328
|
@slash_overlay_disabled = previous_slash_overlay_disabled
|
|
329
329
|
end
|
|
330
330
|
|
|
331
|
+
def update_completion_provider(provider, slash_overlay: true)
|
|
332
|
+
@mutex.synchronize do
|
|
333
|
+
@completion_provider = provider
|
|
334
|
+
@completion_cycle = nil
|
|
335
|
+
@completion_overlay = nil
|
|
336
|
+
@slash_overlay_disabled = !slash_overlay
|
|
337
|
+
render_prompt_locked if @started
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
|
|
331
341
|
def with_prompt_history(history)
|
|
332
342
|
previous_history = @prompt_history
|
|
333
343
|
@prompt_history = history
|
|
@@ -342,6 +352,10 @@ module Kward
|
|
|
342
352
|
@mutex.synchronize { editor_active? }
|
|
343
353
|
end
|
|
344
354
|
|
|
355
|
+
def inline_image_protocol
|
|
356
|
+
@mutex.synchronize { terminal_image_protocol }
|
|
357
|
+
end
|
|
358
|
+
|
|
345
359
|
def update_workspace_root(root, prompt_history: nil)
|
|
346
360
|
@mutex.synchronize do
|
|
347
361
|
expanded_root = File.expand_path(root.to_s.empty? ? Dir.pwd : root)
|
|
@@ -435,6 +449,8 @@ module Kward
|
|
|
435
449
|
reset_history_navigation
|
|
436
450
|
end
|
|
437
451
|
@pending_keys.clear
|
|
452
|
+
@completion_cycle = nil
|
|
453
|
+
@completion_overlay = nil
|
|
438
454
|
@asking = true
|
|
439
455
|
@busy = false
|
|
440
456
|
@queued_count = 0
|
|
@@ -454,11 +470,14 @@ module Kward
|
|
|
454
470
|
render_prompt_locked unless result.is_a?(String) || result == EXIT_INPUT || prompt_action_result?(result)
|
|
455
471
|
end
|
|
456
472
|
end
|
|
473
|
+
perform_pending_transcript_redraw
|
|
457
474
|
return result if result.is_a?(String) || prompt_action_result?(result)
|
|
458
475
|
return nil if result == EXIT_INPUT
|
|
459
476
|
|
|
460
477
|
sleep 0.02 if key.nil?
|
|
461
478
|
end
|
|
479
|
+
ensure
|
|
480
|
+
perform_pending_transcript_redraw
|
|
462
481
|
end
|
|
463
482
|
|
|
464
483
|
def yes?(message, default: false)
|
|
@@ -569,37 +588,59 @@ module Kward
|
|
|
569
588
|
@interactive_state = nil
|
|
570
589
|
restore_composer_snapshot_locked(snapshot)
|
|
571
590
|
redraw_screen_locked if @started
|
|
572
|
-
|
|
591
|
+
flush_output_locked
|
|
573
592
|
end
|
|
574
593
|
end
|
|
575
594
|
|
|
576
|
-
def
|
|
595
|
+
def with_inline_terminal_handoff(&block)
|
|
596
|
+
with_terminal_handoff(inline: true, &block)
|
|
597
|
+
end
|
|
598
|
+
|
|
599
|
+
def with_terminal_handoff(inline: false)
|
|
577
600
|
start
|
|
578
601
|
input = nil
|
|
579
602
|
output = nil
|
|
603
|
+
transition = nil
|
|
580
604
|
@mutex.synchronize do
|
|
581
|
-
|
|
582
|
-
|
|
605
|
+
if inline
|
|
606
|
+
handle_resize_locked
|
|
607
|
+
reserve_composer_region_locked
|
|
608
|
+
move_to_transcript_cursor_locked
|
|
609
|
+
else
|
|
610
|
+
clear_prompt_for_output_locked
|
|
611
|
+
restore_scroll_region_locked
|
|
612
|
+
end
|
|
583
613
|
disable_editor_mouse_reporting(force: true)
|
|
584
|
-
|
|
585
|
-
|
|
614
|
+
print_output_locked(BRACKETED_PASTE_RESTORE)
|
|
615
|
+
print_output_locked(KEYBOARD_PROTOCOL_RESTORE)
|
|
586
616
|
restore_editor_cursor_shape_locked
|
|
587
617
|
set_cursor_visible_locked(true, force: true)
|
|
588
|
-
|
|
618
|
+
flush_output_locked
|
|
589
619
|
restore_console_mode_locked
|
|
590
620
|
input = @input_io
|
|
591
621
|
output = @output_io
|
|
622
|
+
transition = method(:switch_terminal_handoff_to_exclusive)
|
|
623
|
+
@terminal_handoff_mode = inline ? :inline : :exclusive
|
|
624
|
+
@terminal_owner = :child
|
|
592
625
|
end
|
|
593
626
|
|
|
594
|
-
yield(input, output)
|
|
627
|
+
yield(input, output, transition)
|
|
595
628
|
ensure
|
|
596
629
|
@mutex.synchronize do
|
|
630
|
+
@terminal_owner = :kward
|
|
631
|
+
@terminal_handoff_mode = nil
|
|
632
|
+
restore_scroll_region_locked
|
|
633
|
+
print_output_locked(TerminalSequences::SGR_RESET)
|
|
597
634
|
enter_raw_mode_locked
|
|
598
|
-
|
|
599
|
-
|
|
635
|
+
print_output_locked(KEYBOARD_PROTOCOL_ENABLE)
|
|
636
|
+
print_output_locked(BRACKETED_PASTE_ENABLE)
|
|
637
|
+
disable_editor_mouse_reporting(force: true)
|
|
638
|
+
restore_editor_cursor_shape_locked(force: true)
|
|
639
|
+
@cursor_visible = nil
|
|
600
640
|
@last_composer_rows = []
|
|
601
|
-
|
|
602
|
-
|
|
641
|
+
width, height = screen_size
|
|
642
|
+
with_synchronized_output_locked { redraw_screen_locked(width: width, height: height) }
|
|
643
|
+
flush_output_locked
|
|
603
644
|
end
|
|
604
645
|
end
|
|
605
646
|
|
|
@@ -683,6 +724,8 @@ module Kward
|
|
|
683
724
|
|
|
684
725
|
def restore_composer_snapshot_locked(snapshot)
|
|
685
726
|
@composer = snapshot[:composer] || new_composer_state_with_history
|
|
727
|
+
@completion_cycle = nil
|
|
728
|
+
@completion_overlay = nil
|
|
686
729
|
@prompt_label = snapshot[:prompt_label].to_s.empty? ? "You>" : snapshot[:prompt_label].to_s
|
|
687
730
|
self.composer_input = @composer.input
|
|
688
731
|
self.composer_cursor = @composer.cursor
|
|
@@ -811,6 +854,8 @@ module Kward
|
|
|
811
854
|
render_prompt_locked unless [EXIT_INPUT, CANCEL_INPUT].include?(result) || prompt_action_result?(result)
|
|
812
855
|
[EXIT_INPUT, CANCEL_INPUT].include?(result) ? result : result
|
|
813
856
|
end
|
|
857
|
+
ensure
|
|
858
|
+
perform_pending_transcript_redraw
|
|
814
859
|
end
|
|
815
860
|
|
|
816
861
|
def update_assistant_label(label)
|
|
@@ -833,7 +878,7 @@ module Kward
|
|
|
833
878
|
@stream_state.finish_block
|
|
834
879
|
restore_composer_cursor_locked
|
|
835
880
|
end
|
|
836
|
-
|
|
881
|
+
flush_output_locked
|
|
837
882
|
end
|
|
838
883
|
end
|
|
839
884
|
|
|
@@ -855,6 +900,23 @@ module Kward
|
|
|
855
900
|
end
|
|
856
901
|
end
|
|
857
902
|
|
|
903
|
+
def record_transient_terminal_output(text, render: true)
|
|
904
|
+
value = text.to_s
|
|
905
|
+
return if value.empty?
|
|
906
|
+
|
|
907
|
+
@mutex.synchronize do
|
|
908
|
+
append_transcript_buffer(value)
|
|
909
|
+
append_transcript_buffer("\n") unless value.end_with?("\n")
|
|
910
|
+
@stream_state.finish_block
|
|
911
|
+
width, height = screen_size
|
|
912
|
+
remember_transcript_viewport_locked(height)
|
|
913
|
+
next unless render
|
|
914
|
+
|
|
915
|
+
with_synchronized_output_locked { redraw_screen_locked(width: width, height: height) }
|
|
916
|
+
flush_output_locked
|
|
917
|
+
end
|
|
918
|
+
end
|
|
919
|
+
|
|
858
920
|
def write_transcript_delta(delta)
|
|
859
921
|
@mutex.synchronize do
|
|
860
922
|
with_synchronized_output_locked do
|
|
@@ -862,7 +924,7 @@ module Kward
|
|
|
862
924
|
write_transcript_text_locked(delta.to_s)
|
|
863
925
|
restore_composer_cursor_locked unless @restoring_transcript
|
|
864
926
|
end
|
|
865
|
-
|
|
927
|
+
flush_output_locked unless @restoring_transcript
|
|
866
928
|
end
|
|
867
929
|
end
|
|
868
930
|
|
|
@@ -876,7 +938,7 @@ module Kward
|
|
|
876
938
|
@mutex.synchronize do
|
|
877
939
|
width, height = screen_size
|
|
878
940
|
with_synchronized_output_locked { redraw_screen_locked(width: width, height: height) }
|
|
879
|
-
|
|
941
|
+
flush_output_locked
|
|
880
942
|
end
|
|
881
943
|
end
|
|
882
944
|
|
|
@@ -898,12 +960,71 @@ module Kward
|
|
|
898
960
|
@stream_state.reset
|
|
899
961
|
width, height = screen_size
|
|
900
962
|
with_synchronized_output_locked { redraw_screen_locked(width: width, height: height) }
|
|
901
|
-
|
|
963
|
+
flush_output_locked
|
|
902
964
|
end
|
|
903
965
|
end
|
|
904
966
|
|
|
905
967
|
private
|
|
906
968
|
|
|
969
|
+
def switch_terminal_handoff_to_exclusive
|
|
970
|
+
@mutex.synchronize do
|
|
971
|
+
return unless @terminal_owner == :child && @terminal_handoff_mode == :inline
|
|
972
|
+
|
|
973
|
+
@terminal_owner = :kward
|
|
974
|
+
with_synchronized_output_locked do
|
|
975
|
+
clear_prompt_for_output_locked
|
|
976
|
+
restore_scroll_region_locked
|
|
977
|
+
end
|
|
978
|
+
flush_output_locked
|
|
979
|
+
@terminal_handoff_mode = :exclusive
|
|
980
|
+
@terminal_owner = :child
|
|
981
|
+
end
|
|
982
|
+
end
|
|
983
|
+
|
|
984
|
+
def write_transcript_message(message, preserve_composer:)
|
|
985
|
+
@mutex.synchronize do
|
|
986
|
+
text = message.to_s
|
|
987
|
+
if @restoring_transcript
|
|
988
|
+
write_transcript_text_locked(text)
|
|
989
|
+
write_transcript_text_locked("\n") unless text.end_with?("\n")
|
|
990
|
+
@stream_state.finish_block
|
|
991
|
+
next
|
|
992
|
+
end
|
|
993
|
+
|
|
994
|
+
with_synchronized_output_locked do
|
|
995
|
+
if preserve_composer
|
|
996
|
+
prepare_transcript_output_locked
|
|
997
|
+
else
|
|
998
|
+
clear_prompt_for_output_locked
|
|
999
|
+
end
|
|
1000
|
+
write_transcript_text_locked(text)
|
|
1001
|
+
write_transcript_text_locked("\n") unless text.end_with?("\n")
|
|
1002
|
+
@stream_state.finish_block
|
|
1003
|
+
preserve_composer ? restore_composer_cursor_locked : render_prompt_after_output_locked
|
|
1004
|
+
end
|
|
1005
|
+
flush_output_locked
|
|
1006
|
+
end
|
|
1007
|
+
end
|
|
1008
|
+
|
|
1009
|
+
def request_transcript_redraw
|
|
1010
|
+
@transcript_redraw_requested = true
|
|
1011
|
+
end
|
|
1012
|
+
|
|
1013
|
+
def perform_pending_transcript_redraw
|
|
1014
|
+
requested = @mutex.synchronize do
|
|
1015
|
+
pending = @transcript_redraw_requested
|
|
1016
|
+
@transcript_redraw_requested = false
|
|
1017
|
+
pending
|
|
1018
|
+
end
|
|
1019
|
+
return unless requested
|
|
1020
|
+
|
|
1021
|
+
if @redraw_handler
|
|
1022
|
+
@redraw_handler.call
|
|
1023
|
+
else
|
|
1024
|
+
redraw
|
|
1025
|
+
end
|
|
1026
|
+
end
|
|
1027
|
+
|
|
907
1028
|
def prompt_workspace_root
|
|
908
1029
|
@workspace_root
|
|
909
1030
|
end
|
|
@@ -913,6 +1034,8 @@ module Kward
|
|
|
913
1034
|
@file_mention_path_entries_paths = nil
|
|
914
1035
|
@file_mention_path_entries = nil
|
|
915
1036
|
@project_browser_state = nil
|
|
1037
|
+
@image_viewer_state = nil
|
|
1038
|
+
@terminal_image_protocol = nil
|
|
916
1039
|
@project_browser_tree_paths = nil
|
|
917
1040
|
@project_browser_tree = nil
|
|
918
1041
|
@file_overlay_dismissed_token = nil
|
|
@@ -28,6 +28,7 @@ module Kward
|
|
|
28
28
|
{ name: "diff", description: "Open the file changes recorded in this session.", argument_hint: "" },
|
|
29
29
|
{ name: "files", description: "Browse project files.", argument_hint: "" },
|
|
30
30
|
{ name: "shell", description: "Open the embedded Kward shell.", argument_hint: "" },
|
|
31
|
+
{ name: "capture", description: "Run a command with bounded transcript capture.", argument_hint: "<command>" },
|
|
31
32
|
{ name: "scratchpad", description: "Open an unsaved editor buffer.", argument_hint: "[text|markdown|ruby]" },
|
|
32
33
|
{ name: "pty", description: "Run a command in an interactive PTY passthrough session.", argument_hint: "<command>" },
|
|
33
34
|
{ name: "tab", description: "Manage tabs.", argument_hint: "[1-n|move|close|new|name|worktree]" },
|
|
@@ -36,6 +37,7 @@ module Kward
|
|
|
36
37
|
{ name: "sandbox", description: "Inspect or configure model command sandboxing.", argument_hint: "[status|off|read_only|workspace_write|network allow|network deny]" },
|
|
37
38
|
{ name: "hooks", description: "Inspect lifecycle hooks.", argument_hint: "[list|events|logs|doctor|trust|untrust]" },
|
|
38
39
|
{ name: "skill", description: "Activate a skill or capture one from a saved session.", argument_hint: "<name>|capture" },
|
|
40
|
+
{ name: "skills", description: "Review project skill trust.", argument_hint: "[status|trust|untrust]" },
|
|
39
41
|
{ name: "memory", description: "Inspect and manage Kward memory.", argument_hint: "[enable|disable|auto-summary|core|add|list|forget|promote|relax|inspect|why|summarize]" }
|
|
40
42
|
].freeze
|
|
41
43
|
BUILTIN_RESERVED_COMMAND_NAMES = BUILTIN_COMMANDS.map { |command| command[:name] }.freeze
|
data/lib/kward/prompts.rb
CHANGED
|
@@ -22,22 +22,22 @@ module Kward
|
|
|
22
22
|
# @param plugin_context [String, nil] trusted plugin-provided instructions
|
|
23
23
|
# @return [Hash] role/content system message
|
|
24
24
|
# @api public
|
|
25
|
-
def system_message(workspace_root: Dir.pwd, include_workspace_personality: true, model: nil, reasoning_effort: nil, now: Time.now, memory_context: nil, plugin_context: nil, execution_profile_context: nil)
|
|
25
|
+
def system_message(workspace_root: Dir.pwd, include_workspace_personality: true, model: nil, reasoning_effort: nil, now: Time.now, memory_context: nil, plugin_context: nil, execution_profile_context: nil, project_skill_paths: nil)
|
|
26
26
|
{
|
|
27
27
|
role: "system",
|
|
28
|
-
content: prompt_parts(workspace_root: workspace_root, include_workspace_personality: include_workspace_personality, model: model, reasoning_effort: reasoning_effort, now: now, memory_context: memory_context, plugin_context: plugin_context, execution_profile_context: execution_profile_context).compact.join("\n\n")
|
|
28
|
+
content: prompt_parts(workspace_root: workspace_root, include_workspace_personality: include_workspace_personality, model: model, reasoning_effort: reasoning_effort, now: now, memory_context: memory_context, plugin_context: plugin_context, execution_profile_context: execution_profile_context, project_skill_paths: project_skill_paths).compact.join("\n\n")
|
|
29
29
|
}
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
-
def prompt_parts(workspace_root: Dir.pwd, include_workspace_personality: true, model: nil, reasoning_effort: nil, now: Time.now, memory_context: nil, plugin_context: nil, execution_profile_context: nil)
|
|
33
|
-
prompt_sections(workspace_root: workspace_root, include_workspace_personality: include_workspace_personality, model: model, reasoning_effort: reasoning_effort, now: now, memory_context: memory_context, plugin_context: plugin_context, execution_profile_context: execution_profile_context).map { |section| section[:content] }
|
|
32
|
+
def prompt_parts(workspace_root: Dir.pwd, include_workspace_personality: true, model: nil, reasoning_effort: nil, now: Time.now, memory_context: nil, plugin_context: nil, execution_profile_context: nil, project_skill_paths: nil)
|
|
33
|
+
prompt_sections(workspace_root: workspace_root, include_workspace_personality: include_workspace_personality, model: model, reasoning_effort: reasoning_effort, now: now, memory_context: memory_context, plugin_context: plugin_context, execution_profile_context: execution_profile_context, project_skill_paths: project_skill_paths).map { |section| section[:content] }
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
# Returns labeled prompt sections for inspection by CLI and RPC frontends.
|
|
37
37
|
#
|
|
38
38
|
# @return [Array<Hash>] section hashes with label, content, and optional source
|
|
39
39
|
# @api public
|
|
40
|
-
def prompt_sections(workspace_root: Dir.pwd, include_workspace_personality: true, model: nil, reasoning_effort: nil, now: Time.now, memory_context: nil, plugin_context: nil, execution_profile_context: nil)
|
|
40
|
+
def prompt_sections(workspace_root: Dir.pwd, include_workspace_personality: true, model: nil, reasoning_effort: nil, now: Time.now, memory_context: nil, plugin_context: nil, execution_profile_context: nil, project_skill_paths: nil)
|
|
41
41
|
return replacement_prompt_sections if ConfigFiles.replacement_system_prompt?
|
|
42
42
|
|
|
43
43
|
sections = [prompt_section("Built-in system prompt", base_prompt)]
|
|
@@ -48,7 +48,7 @@ module Kward
|
|
|
48
48
|
sections << prompt_section("Persona", persona_prompt(workspace_root, model: model, reasoning_effort: reasoning_effort, now: now))
|
|
49
49
|
sections << prompt_section("Plugin context", plugin_context) unless plugin_context.to_s.empty?
|
|
50
50
|
end
|
|
51
|
-
skills = ConfigFiles.skills(workspace_root: workspace_root)
|
|
51
|
+
skills = ConfigFiles.skills(workspace_root: workspace_root, project_skill_paths: project_skill_paths)
|
|
52
52
|
sections << prompt_section("Configured skills", skills_prompt(skills), source: skills.empty? ? nil : File.join(ConfigFiles.config_dir, "skills"))
|
|
53
53
|
sections << prompt_section(workspace_agents_context_label(workspace_root), workspace_agents_context(workspace_root), source: ConfigFiles.workspace_agents_file?(workspace_root) ? ConfigFiles.workspace_agents_path(workspace_root) : nil)
|
|
54
54
|
sections.compact
|