kward 0.67.1 → 0.69.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/.github/workflows/pages.yml +48 -0
- data/.yardopts +1 -0
- data/CHANGELOG.md +54 -0
- data/Gemfile.lock +8 -2
- data/README.md +37 -30
- data/Rakefile +14 -1
- data/doc/authentication.md +84 -43
- data/doc/code-search.md +55 -28
- data/doc/configuration.md +27 -2
- data/doc/extensibility.md +90 -129
- data/doc/getting-started.md +53 -57
- data/doc/memory.md +51 -118
- data/doc/personas.md +417 -0
- data/doc/plugins.md +55 -99
- data/doc/releasing.md +10 -9
- data/doc/rpc.md +7 -7
- data/doc/usage.md +125 -141
- data/doc/web-search.md +80 -14
- data/exe/kward +2 -0
- data/kward.gemspec +4 -0
- data/lib/kward/agent.rb +30 -3
- data/lib/kward/ansi.rb +3 -0
- data/lib/kward/auth/anthropic_oauth.rb +291 -0
- data/lib/kward/auth/file.rb +2 -0
- data/lib/kward/auth/github_oauth.rb +3 -0
- data/lib/kward/auth/openai_oauth.rb +4 -0
- data/lib/kward/auth/openrouter_api_key.rb +2 -0
- data/lib/kward/cancellation.rb +3 -0
- data/lib/kward/cli/auth_commands.rb +82 -0
- data/lib/kward/cli/commands.rb +229 -0
- data/lib/kward/cli/compaction.rb +25 -0
- data/lib/kward/cli/doctor.rb +121 -0
- data/lib/kward/cli/interactive_turn.rb +227 -0
- data/lib/kward/cli/memory_commands.rb +133 -0
- data/lib/kward/cli/plugins.rb +112 -0
- data/lib/kward/cli/prompt_interface.rb +134 -0
- data/lib/kward/cli/rendering.rb +378 -0
- data/lib/kward/cli/runtime_helpers.rb +170 -0
- data/lib/kward/cli/sessions.rb +376 -0
- data/lib/kward/cli/settings.rb +669 -0
- data/lib/kward/cli/slash_commands.rb +114 -0
- data/lib/kward/cli/stats.rb +64 -0
- data/lib/kward/cli/sysprompt.rb +57 -0
- data/lib/kward/cli/tool_summaries.rb +157 -0
- data/lib/kward/cli.rb +52 -2792
- data/lib/kward/cli_transcript_formatter.rb +40 -12
- data/lib/kward/clipboard.rb +1 -0
- data/lib/kward/compaction/file_operation_tracker.rb +3 -0
- data/lib/kward/compactor.rb +31 -9
- data/lib/kward/config_files.rb +78 -34
- data/lib/kward/conversation.rb +110 -13
- data/lib/kward/events.rb +2 -0
- data/lib/kward/export_path.rb +2 -0
- data/lib/kward/image_attachments.rb +2 -0
- data/lib/kward/markdown_transcript.rb +2 -0
- data/lib/kward/memory/manager.rb +144 -14
- data/lib/kward/message_access.rb +29 -2
- data/lib/kward/message_text.rb +45 -0
- data/lib/kward/model/chat_invocation.rb +2 -0
- data/lib/kward/model/client.rb +295 -77
- data/lib/kward/model/context_overflow.rb +2 -0
- data/lib/kward/model/context_usage.rb +14 -10
- data/lib/kward/model/model_info.rb +160 -4
- data/lib/kward/model/payloads.rb +254 -22
- data/lib/kward/model/retry_message.rb +2 -0
- data/lib/kward/model/stream_parser.rb +387 -25
- data/lib/kward/pan/server.rb +3 -1
- data/lib/kward/plugin_registry.rb +12 -0
- data/lib/kward/private_file.rb +2 -0
- data/lib/kward/prompt_interface/banner.rb +3 -0
- data/lib/kward/prompt_interface/composer_controller.rb +262 -0
- data/lib/kward/prompt_interface/composer_renderer.rb +172 -0
- data/lib/kward/prompt_interface/composer_state.rb +221 -0
- data/lib/kward/prompt_interface/key_handler.rb +365 -0
- data/lib/kward/prompt_interface/layout.rb +31 -0
- data/lib/kward/prompt_interface/overlay_renderer.rb +111 -0
- data/lib/kward/prompt_interface/prompt_renderer.rb +91 -0
- data/lib/kward/prompt_interface/question_prompt.rb +328 -0
- data/lib/kward/prompt_interface/runtime_state.rb +59 -0
- data/lib/kward/prompt_interface/screen.rb +186 -0
- data/lib/kward/prompt_interface/selection_prompt.rb +242 -0
- data/lib/kward/prompt_interface/slash_overlay.rb +102 -0
- data/lib/kward/prompt_interface/stream_state.rb +65 -0
- data/lib/kward/prompt_interface/transcript_buffer.rb +85 -0
- data/lib/kward/prompt_interface/transcript_renderer.rb +151 -0
- data/lib/kward/prompt_interface.rb +69 -1832
- data/lib/kward/prompts/commands.rb +2 -0
- data/lib/kward/prompts/templates.rb +3 -0
- data/lib/kward/prompts.rb +63 -7
- data/lib/kward/question_contract.rb +66 -0
- data/lib/kward/resources/avatar_kward_logo.rb +2 -0
- data/lib/kward/resources/pixel_logo.rb +2 -0
- data/lib/kward/rpc/attachment_normalizer.rb +60 -0
- data/lib/kward/rpc/auth_manager.rb +65 -11
- data/lib/kward/rpc/config_manager.rb +11 -0
- data/lib/kward/rpc/prompt_bridge.rb +5 -26
- data/lib/kward/rpc/redactor.rb +3 -0
- data/lib/kward/rpc/runtime_payloads.rb +4 -1
- data/lib/kward/rpc/server.rb +43 -11
- data/lib/kward/rpc/session_manager.rb +139 -347
- data/lib/kward/rpc/session_metrics.rb +68 -0
- data/lib/kward/rpc/session_tree.rb +48 -0
- data/lib/kward/rpc/session_tree_rows.rb +208 -0
- data/lib/kward/rpc/tool_event_normalizer.rb +3 -0
- data/lib/kward/rpc/tool_metadata.rb +3 -0
- data/lib/kward/rpc/transcript_normalizer.rb +50 -0
- data/lib/kward/rpc/transport.rb +3 -0
- data/lib/kward/session_diff.rb +2 -0
- data/lib/kward/session_store.rb +154 -25
- data/lib/kward/session_trash.rb +1 -0
- data/lib/kward/session_tree_renderer.rb +8 -41
- data/lib/kward/session_tree_tool_display.rb +56 -0
- data/lib/kward/skills/registry.rb +3 -0
- data/lib/kward/starter_pack_installer.rb +3 -2
- data/lib/kward/steering.rb +2 -0
- data/lib/kward/telemetry/logger.rb +3 -0
- data/lib/kward/telemetry/stats.rb +3 -0
- data/lib/kward/tools/ask_user_question.rb +20 -32
- data/lib/kward/tools/base.rb +8 -0
- data/lib/kward/tools/code_search.rb +5 -0
- data/lib/kward/tools/edit_file.rb +5 -0
- data/lib/kward/tools/fetch_content.rb +41 -0
- data/lib/kward/tools/fetch_raw.rb +40 -0
- data/lib/kward/tools/list_directory.rb +5 -0
- data/lib/kward/tools/read_file.rb +5 -0
- data/lib/kward/tools/read_skill.rb +5 -0
- data/lib/kward/tools/registry.rb +42 -4
- data/lib/kward/tools/run_shell_command.rb +5 -0
- data/lib/kward/tools/search/code.rb +7 -0
- data/lib/kward/tools/search/web.rb +20 -17
- data/lib/kward/tools/search/web_fetch.rb +202 -0
- data/lib/kward/tools/tool_call.rb +27 -5
- data/lib/kward/tools/web_search.rb +7 -1
- data/lib/kward/tools/write_file.rb +5 -0
- data/lib/kward/transcript_export.rb +2 -0
- data/lib/kward/version.rb +2 -1
- data/lib/kward/workspace.rb +45 -5
- data/templates/default/fulldoc/html/css/kward.css +1501 -0
- data/templates/default/fulldoc/html/images/kward_logo.png +0 -0
- data/templates/default/fulldoc/html/js/kward.js +296 -0
- data/templates/default/fulldoc/html/setup.rb +8 -0
- data/templates/default/layout/html/breadcrumb.erb +11 -0
- data/templates/default/layout/html/layout.erb +141 -0
- data/templates/default/layout/html/setup.rb +139 -0
- metadata +56 -1
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
# Namespace for the Kward CLI agent runtime.
|
|
2
|
+
module Kward
|
|
3
|
+
# Keyboard sequence dispatcher for composer and overlay input.
|
|
4
|
+
class PromptInterface
|
|
5
|
+
# Keyboard sequence handling for the terminal prompt interface.
|
|
6
|
+
module KeyHandler
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def read_key(nonblock: false)
|
|
10
|
+
pending = @pending_keys.shift unless @pending_keys.empty?
|
|
11
|
+
return pending if pending
|
|
12
|
+
|
|
13
|
+
@reader.read_keypress(echo: false, raw: true, nonblock: nonblock)
|
|
14
|
+
rescue TTY::Reader::InputInterrupt
|
|
15
|
+
"\x03"
|
|
16
|
+
rescue IO::WaitReadable, Errno::EAGAIN, Errno::EWOULDBLOCK
|
|
17
|
+
nil
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def handle_key(key)
|
|
21
|
+
return submit_input if key.nil?
|
|
22
|
+
return if handle_bracketed_paste_key(key)
|
|
23
|
+
|
|
24
|
+
csi_result = handle_csi_u_key(key)
|
|
25
|
+
return csi_result unless csi_result == false
|
|
26
|
+
return if handle_shift_enter_key(key)
|
|
27
|
+
if key.is_a?(String) && key.length > 1
|
|
28
|
+
token = next_key_token(key)
|
|
29
|
+
if token.length < key.length
|
|
30
|
+
queue_pending_keys(key[token.length..])
|
|
31
|
+
return handle_key(token)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
binding_result = handle_composer_key_binding(key)
|
|
36
|
+
return binding_result unless binding_result == false
|
|
37
|
+
|
|
38
|
+
key_name = @reader.console.keys[key]
|
|
39
|
+
case key_name
|
|
40
|
+
when :return, :enter
|
|
41
|
+
submit_input
|
|
42
|
+
when :backspace
|
|
43
|
+
delete_before_cursor
|
|
44
|
+
when :delete
|
|
45
|
+
delete_at_cursor
|
|
46
|
+
when :ctrl_d
|
|
47
|
+
delete_at_cursor_or_exit
|
|
48
|
+
when :ctrl_c
|
|
49
|
+
cancel_input_or_interrupt
|
|
50
|
+
when :ctrl_a
|
|
51
|
+
move_to_start_of_line
|
|
52
|
+
when :ctrl_e
|
|
53
|
+
move_to_end_of_line
|
|
54
|
+
when :ctrl_b
|
|
55
|
+
move_cursor_left
|
|
56
|
+
when :ctrl_f
|
|
57
|
+
move_cursor_right
|
|
58
|
+
when :ctrl_w
|
|
59
|
+
delete_word_before_cursor
|
|
60
|
+
when :ctrl_u
|
|
61
|
+
kill_line_before_cursor
|
|
62
|
+
when :ctrl_k
|
|
63
|
+
kill_line_after_cursor
|
|
64
|
+
when :ctrl_y
|
|
65
|
+
yank_kill_buffer
|
|
66
|
+
when :ctrl_l
|
|
67
|
+
redraw_screen_locked
|
|
68
|
+
when :left
|
|
69
|
+
move_cursor_left
|
|
70
|
+
when :right
|
|
71
|
+
move_cursor_right
|
|
72
|
+
when :home
|
|
73
|
+
move_to_start_of_line
|
|
74
|
+
when :end
|
|
75
|
+
move_to_end_of_line
|
|
76
|
+
when :up
|
|
77
|
+
slash_overlay_visible? ? select_previous_slash_command : recall_previous_history
|
|
78
|
+
when :down
|
|
79
|
+
slash_overlay_visible? ? select_next_slash_command : recall_next_history
|
|
80
|
+
else
|
|
81
|
+
case key
|
|
82
|
+
when "\n", "\r"
|
|
83
|
+
submit_input
|
|
84
|
+
when "\t"
|
|
85
|
+
complete_selected_slash_command || insert_key(key)
|
|
86
|
+
when "\b", "\x7F"
|
|
87
|
+
delete_before_cursor
|
|
88
|
+
when "\x04"
|
|
89
|
+
delete_at_cursor_or_exit
|
|
90
|
+
when "\x03"
|
|
91
|
+
cancel_input_or_interrupt
|
|
92
|
+
when "\e"
|
|
93
|
+
handle_escape_sequence
|
|
94
|
+
else
|
|
95
|
+
insert_key(key)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def cancel_input_or_interrupt
|
|
101
|
+
return CANCEL_INPUT if @busy
|
|
102
|
+
|
|
103
|
+
raise Interrupt
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def handle_escape_sequence
|
|
107
|
+
pending_sequence = read_pending_escape_sequence
|
|
108
|
+
return true if pending_sequence.empty? && dismiss_slash_overlay
|
|
109
|
+
|
|
110
|
+
full_sequence = "\e#{pending_sequence}"
|
|
111
|
+
sequence = next_key_token(full_sequence)
|
|
112
|
+
queue_pending_keys(full_sequence[sequence.length..]) if full_sequence.length > sequence.length
|
|
113
|
+
return true if sequence == "\e" && dismiss_slash_overlay
|
|
114
|
+
return true if handle_shift_enter_key(sequence)
|
|
115
|
+
|
|
116
|
+
binding_result = handle_composer_key_binding(sequence)
|
|
117
|
+
return binding_result unless binding_result == false
|
|
118
|
+
|
|
119
|
+
key_name = @reader.console.keys[sequence]
|
|
120
|
+
case key_name
|
|
121
|
+
when :up
|
|
122
|
+
slash_overlay_visible? ? select_previous_slash_command : recall_previous_history
|
|
123
|
+
when :down
|
|
124
|
+
slash_overlay_visible? ? select_next_slash_command : recall_next_history
|
|
125
|
+
when :left
|
|
126
|
+
move_cursor_left
|
|
127
|
+
when :right
|
|
128
|
+
move_cursor_right
|
|
129
|
+
when :home
|
|
130
|
+
move_to_start_of_line
|
|
131
|
+
when :end
|
|
132
|
+
move_to_end_of_line
|
|
133
|
+
when :delete
|
|
134
|
+
delete_at_cursor
|
|
135
|
+
end
|
|
136
|
+
true
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def handle_bracketed_paste_key(key)
|
|
140
|
+
text = key.to_s
|
|
141
|
+
return false unless text.start_with?(BRACKETED_PASTE_START)
|
|
142
|
+
|
|
143
|
+
pasted = text[BRACKETED_PASTE_START.length..] || ""
|
|
144
|
+
until pasted.include?(BRACKETED_PASTE_END)
|
|
145
|
+
chunk = @reader.read_keypress(echo: false, raw: true)
|
|
146
|
+
break if chunk.nil?
|
|
147
|
+
|
|
148
|
+
pasted << chunk.to_s
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
content, remaining = pasted.split(BRACKETED_PASTE_END, 2)
|
|
152
|
+
insert_paste(normalize_paste(content || ""))
|
|
153
|
+
queue_pending_keys(remaining) if remaining && !remaining.empty?
|
|
154
|
+
true
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def normalize_paste(content)
|
|
158
|
+
content.gsub("\r\n", "\n").gsub("\r", "\n")
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def handle_csi_u_key(key)
|
|
162
|
+
match = key.to_s.match(/\A\e\[(\d+)(?:;([\d:]+))?u/)
|
|
163
|
+
return false unless match
|
|
164
|
+
|
|
165
|
+
sequence = match[0]
|
|
166
|
+
code = match[1].to_i
|
|
167
|
+
modifier = (match[2] || "1").split(":", 2).first.to_i
|
|
168
|
+
queue_pending_keys(key[sequence.length..]) if key.length > sequence.length
|
|
169
|
+
|
|
170
|
+
case code
|
|
171
|
+
when 13
|
|
172
|
+
modifier == 2 ? insert_string("\n") : submit_input
|
|
173
|
+
when 27
|
|
174
|
+
dismiss_slash_overlay || false
|
|
175
|
+
when 8, 127
|
|
176
|
+
alt_modifier?(modifier) ? delete_word_before_cursor : delete_before_cursor
|
|
177
|
+
nil
|
|
178
|
+
when 4
|
|
179
|
+
delete_at_cursor_or_exit
|
|
180
|
+
else
|
|
181
|
+
handle_modified_csi_u_key(code, modifier)
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def handle_modified_csi_u_key(code, modifier)
|
|
186
|
+
return false unless ctrl_modifier?(modifier) || alt_modifier?(modifier)
|
|
187
|
+
|
|
188
|
+
normalized_code = code.to_i.chr.downcase.ord rescue code
|
|
189
|
+
if ctrl_modifier?(modifier)
|
|
190
|
+
case normalized_code
|
|
191
|
+
when 97
|
|
192
|
+
move_to_start_of_line
|
|
193
|
+
when 98
|
|
194
|
+
move_cursor_left
|
|
195
|
+
when 99
|
|
196
|
+
cancel_input_or_interrupt
|
|
197
|
+
when 100
|
|
198
|
+
delete_at_cursor_or_exit
|
|
199
|
+
when 101
|
|
200
|
+
move_to_end_of_line
|
|
201
|
+
when 102
|
|
202
|
+
move_cursor_right
|
|
203
|
+
when 104
|
|
204
|
+
delete_before_cursor
|
|
205
|
+
when 107
|
|
206
|
+
kill_line_after_cursor
|
|
207
|
+
when 108
|
|
208
|
+
redraw_screen_locked
|
|
209
|
+
when 117
|
|
210
|
+
kill_line_before_cursor
|
|
211
|
+
when 119
|
|
212
|
+
delete_word_before_cursor
|
|
213
|
+
when 121
|
|
214
|
+
yank_kill_buffer
|
|
215
|
+
else
|
|
216
|
+
false
|
|
217
|
+
end
|
|
218
|
+
elsif alt_modifier?(modifier)
|
|
219
|
+
case normalized_code
|
|
220
|
+
when 98
|
|
221
|
+
move_to_previous_word
|
|
222
|
+
when 100
|
|
223
|
+
delete_word_after_cursor
|
|
224
|
+
when 102
|
|
225
|
+
move_to_next_word
|
|
226
|
+
else
|
|
227
|
+
false
|
|
228
|
+
end
|
|
229
|
+
else
|
|
230
|
+
false
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def ctrl_modifier?(modifier)
|
|
235
|
+
((modifier.to_i - 1) & 4).positive?
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def alt_modifier?(modifier)
|
|
239
|
+
((modifier.to_i - 1) & 2).positive?
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def handle_shift_enter_key(key)
|
|
243
|
+
sequence = shift_enter_sequence_for(key)
|
|
244
|
+
return false unless sequence
|
|
245
|
+
|
|
246
|
+
insert_string("\n")
|
|
247
|
+
queue_pending_keys(key[sequence.length..]) if key.length > sequence.length
|
|
248
|
+
true
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def queue_pending_keys(keys)
|
|
252
|
+
remaining = keys.to_s
|
|
253
|
+
until remaining.empty?
|
|
254
|
+
token = next_key_token(remaining)
|
|
255
|
+
@pending_keys << token
|
|
256
|
+
remaining = remaining[token.length..] || ""
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def next_key_token(keys)
|
|
261
|
+
text = keys.to_s
|
|
262
|
+
text.match(/\A\e\[[0-9;:]*[A-Za-z~]/)&.[](0) ||
|
|
263
|
+
text.match(/\A\eO[A-Za-z]/)&.[](0) ||
|
|
264
|
+
shift_enter_sequence_for(text) ||
|
|
265
|
+
(text.start_with?("\e") && text.length > 1 && alt_key_sequence?(text[1]) ? text[0, 2] : text[0, 1])
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def alt_key_sequence?(char)
|
|
269
|
+
char = char.to_s
|
|
270
|
+
char.match?(/[[:alpha:]]/) || char == "\b" || char == "\x7F"
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def shift_enter_sequence_for(key)
|
|
274
|
+
return nil unless key.is_a?(String)
|
|
275
|
+
|
|
276
|
+
SHIFT_ENTER_SEQUENCES.find { |sequence| key.start_with?(sequence) }
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def read_pending_escape_sequence
|
|
280
|
+
sequence = +""
|
|
281
|
+
until @pending_keys.empty?
|
|
282
|
+
sequence << @pending_keys.shift.to_s
|
|
283
|
+
end
|
|
284
|
+
while (char = @reader.read_keypress(echo: false, raw: true, nonblock: true))
|
|
285
|
+
sequence << char.to_s
|
|
286
|
+
end
|
|
287
|
+
sequence
|
|
288
|
+
rescue IO::WaitReadable, Errno::EAGAIN, Errno::EWOULDBLOCK
|
|
289
|
+
sequence
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def handle_composer_key_binding(key)
|
|
293
|
+
case key
|
|
294
|
+
when "\x01"
|
|
295
|
+
move_to_start_of_line
|
|
296
|
+
when "\x02"
|
|
297
|
+
move_cursor_left
|
|
298
|
+
when "\x04"
|
|
299
|
+
delete_at_cursor_or_exit
|
|
300
|
+
when "\x05"
|
|
301
|
+
move_to_end_of_line
|
|
302
|
+
when "\x06"
|
|
303
|
+
move_cursor_right
|
|
304
|
+
when "\x0B"
|
|
305
|
+
kill_line_after_cursor
|
|
306
|
+
when "\x0C"
|
|
307
|
+
redraw_screen_locked
|
|
308
|
+
when "\x15"
|
|
309
|
+
kill_line_before_cursor
|
|
310
|
+
when "\x17"
|
|
311
|
+
delete_word_before_cursor
|
|
312
|
+
when "\x19"
|
|
313
|
+
yank_kill_buffer
|
|
314
|
+
when "\e[D", "\eOD"
|
|
315
|
+
move_cursor_left
|
|
316
|
+
when "\e[C", "\eOC"
|
|
317
|
+
move_cursor_right
|
|
318
|
+
when "\e[H", "\eOH", "\e[1~", "\e[7~"
|
|
319
|
+
move_to_start_of_line
|
|
320
|
+
when "\e[F", "\eOF", "\e[4~", "\e[8~"
|
|
321
|
+
move_to_end_of_line
|
|
322
|
+
when "\e[3~"
|
|
323
|
+
delete_at_cursor
|
|
324
|
+
when "\eb", "\eB"
|
|
325
|
+
move_to_previous_word
|
|
326
|
+
when "\ef", "\eF"
|
|
327
|
+
move_to_next_word
|
|
328
|
+
when "\ed", "\eD"
|
|
329
|
+
delete_word_after_cursor
|
|
330
|
+
when "\e\b", "\e\x7F"
|
|
331
|
+
delete_word_before_cursor
|
|
332
|
+
else
|
|
333
|
+
handle_modified_ansi_key(key) || false
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def handle_modified_ansi_key(key)
|
|
338
|
+
match = key.to_s.match(/\A\e\[(\d+);(\d+)([CDFH])\z/)
|
|
339
|
+
if match
|
|
340
|
+
modifier = match[2].to_i
|
|
341
|
+
final = match[3]
|
|
342
|
+
return false unless alt_modifier?(modifier)
|
|
343
|
+
|
|
344
|
+
case final
|
|
345
|
+
when "C"
|
|
346
|
+
move_to_next_word
|
|
347
|
+
when "D"
|
|
348
|
+
move_to_previous_word
|
|
349
|
+
when "F"
|
|
350
|
+
move_to_end_of_line
|
|
351
|
+
when "H"
|
|
352
|
+
move_to_start_of_line
|
|
353
|
+
else
|
|
354
|
+
false
|
|
355
|
+
end
|
|
356
|
+
elsif (match = key.to_s.match(/\A\e\[3;(\d+)~\z/))
|
|
357
|
+
alt_modifier?(match[1].to_i) ? delete_word_after_cursor : delete_at_cursor
|
|
358
|
+
else
|
|
359
|
+
false
|
|
360
|
+
end
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Namespace for the Kward CLI agent runtime.
|
|
2
|
+
module Kward
|
|
3
|
+
# Layout calculations for terminal rows and overlay placement.
|
|
4
|
+
class PromptInterface
|
|
5
|
+
# Terminal layout calculations for transcript, overlays, footer, and composer.
|
|
6
|
+
module Layout
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def banner_rows(width)
|
|
10
|
+
@banner.rows(width)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def banner_logo_rows
|
|
14
|
+
@banner.logo_rows(screen_width)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def transcript_redraw_row_count(height = screen_height)
|
|
18
|
+
[[@transcript_viewport_rows, transcript_bottom_row(height)].max, height].min
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def composer_top_row(height = screen_height)
|
|
22
|
+
[height - @reserved_rows + 1, 1].max
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def transcript_bottom_row(height = screen_height)
|
|
26
|
+
[height - @reserved_rows, 1].max
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Namespace for the Kward CLI agent runtime.
|
|
2
|
+
module Kward
|
|
3
|
+
# Shared overlay drawing helpers for prompt UI popups.
|
|
4
|
+
class PromptInterface
|
|
5
|
+
# Renderer for selection, slash-command, and question overlays.
|
|
6
|
+
module OverlayRenderer
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def active_overlay_rows(width, height: screen_height)
|
|
10
|
+
return question_overlay_rows(width) if @question_state
|
|
11
|
+
return selection_overlay_rows(width, height: height) if @select_state
|
|
12
|
+
|
|
13
|
+
slash_overlay_rows(width, height: height)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def overlay_card_rows(title, content_rows, width)
|
|
17
|
+
card_width = overlay_card_width(width)
|
|
18
|
+
inner_width = [card_width - 4, 1].max
|
|
19
|
+
rows = [overlay_top_border(title, card_width)]
|
|
20
|
+
rows.concat(content_rows.map { |row| overlay_content_row(row, inner_width) })
|
|
21
|
+
rows << overlay_bottom_border(card_width)
|
|
22
|
+
rows.map { |row| align_overlay_row(row, width) }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def overlay_card_width(width)
|
|
26
|
+
return width if width < 32
|
|
27
|
+
return width if @overlay_settings["width"] == "maximum"
|
|
28
|
+
|
|
29
|
+
[[width - 4, 32].max, 96].min
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def overlay_top_border(title, card_width)
|
|
33
|
+
title = visible_truncate(title.to_s, [card_width - 4, 1].max)
|
|
34
|
+
plain_length = ANSI.strip(title).length
|
|
35
|
+
colored("╭", :primary_green) + " #{colored(title, :bright_accent_green, :bold)} " + colored("─" * [card_width - plain_length - 4, 0].max, :primary_green) + colored("╮", :primary_green)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def overlay_bottom_border(card_width)
|
|
39
|
+
colored("╰#{"─" * [card_width - 2, 0].max}╯", :primary_green)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def overlay_content_row(row, inner_width)
|
|
43
|
+
text = visible_truncate(row[:text], inner_width)
|
|
44
|
+
text = colored(text, :bright_accent_green, :bold) if row[:selected]
|
|
45
|
+
colored("│", :primary_green) + " " + visible_ljust(text, inner_width) + " " + colored("│", :primary_green)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def overlay_text_line(text, style = nil)
|
|
49
|
+
rendered = case style
|
|
50
|
+
when :bold
|
|
51
|
+
colored(text.to_s, :bold)
|
|
52
|
+
when :muted
|
|
53
|
+
colored(text.to_s, :gray)
|
|
54
|
+
else
|
|
55
|
+
text.to_s
|
|
56
|
+
end
|
|
57
|
+
{ text: rendered }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def overlay_blank_line
|
|
61
|
+
{ text: "" }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def overlay_choice_line(text, selected: false)
|
|
65
|
+
{ text: "#{selected ? "›" : " "} #{text}", selected: selected }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def align_overlay_row(row, width)
|
|
69
|
+
plain_length = ANSI.strip(row).length
|
|
70
|
+
padding = [width - plain_length, 0].max
|
|
71
|
+
left = overlay_left_padding(width, plain_length)
|
|
72
|
+
right = padding - left
|
|
73
|
+
(" " * left) + row + (" " * right)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def overlay_left_padding(width, row_width)
|
|
77
|
+
padding = [width - row_width, 0].max
|
|
78
|
+
case @overlay_settings["alignment"]
|
|
79
|
+
when "left"
|
|
80
|
+
0
|
|
81
|
+
when "right"
|
|
82
|
+
padding
|
|
83
|
+
else
|
|
84
|
+
padding / 2
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def normalize_overlay_settings(settings)
|
|
89
|
+
values = { "alignment" => "center", "width" => "capped" }
|
|
90
|
+
source = settings.is_a?(Hash) ? settings : {}
|
|
91
|
+
alignment = (source[:alignment] || source["alignment"]).to_s
|
|
92
|
+
width = (source[:width] || source["width"]).to_s
|
|
93
|
+
values["alignment"] = alignment if %w[left center right].include?(alignment)
|
|
94
|
+
values["width"] = width if %w[capped maximum].include?(width)
|
|
95
|
+
values
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def visible_ljust(text, width)
|
|
99
|
+
text.to_s + (" " * [width - ANSI.strip(text.to_s).length, 0].max)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def visible_truncate(text, width)
|
|
103
|
+
plain = ANSI.strip(text.to_s)
|
|
104
|
+
return text.to_s if plain.length <= width
|
|
105
|
+
|
|
106
|
+
plain[0, width]
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Namespace for the Kward CLI agent runtime.
|
|
2
|
+
module Kward
|
|
3
|
+
# Prompt label and composer chrome renderer.
|
|
4
|
+
class PromptInterface
|
|
5
|
+
# Renderer for prompt labels and composer prompt chrome.
|
|
6
|
+
module PromptRenderer
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def render_prompt_locked
|
|
10
|
+
return unless @started && @asking
|
|
11
|
+
|
|
12
|
+
handle_resize_locked
|
|
13
|
+
width, height = screen_size
|
|
14
|
+
rows, cursor_row, cursor_col = composer_layout(width, height)
|
|
15
|
+
ensure_scroll_region_locked(rows.length, width: width, height: height)
|
|
16
|
+
@rendered_rows = rows.length
|
|
17
|
+
render_composer_rows_locked(rows, height: height)
|
|
18
|
+
@cursor_rendered_row = cursor_row
|
|
19
|
+
@last_width = width
|
|
20
|
+
@last_height = height
|
|
21
|
+
move_to_screen(composer_top_row(height) + cursor_row, cursor_col + 1)
|
|
22
|
+
render_cursor_visibility_locked
|
|
23
|
+
@output_io.flush
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def render_prompt_after_output_locked
|
|
27
|
+
render_prompt_locked
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def clear_prompt_locked
|
|
31
|
+
handle_resize_locked
|
|
32
|
+
width, height = screen_size
|
|
33
|
+
clear_composer_region_locked(height: height)
|
|
34
|
+
@rendered_rows = 0
|
|
35
|
+
@cursor_rendered_row = 0
|
|
36
|
+
redraw_transcript_locked(width: width, height: height)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def clear_prompt_for_output_locked
|
|
40
|
+
handle_resize_locked
|
|
41
|
+
width, height = screen_size
|
|
42
|
+
reserve_composer_region_locked(width: width, height: height) if @started && @asking
|
|
43
|
+
clear_composer_region_locked(height: height)
|
|
44
|
+
@rendered_rows = 0
|
|
45
|
+
@cursor_rendered_row = 0
|
|
46
|
+
move_to_transcript_cursor_locked(width: width, height: height) if @started
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def prepare_transcript_output_locked
|
|
50
|
+
handle_resize_locked
|
|
51
|
+
width, height = screen_size
|
|
52
|
+
hide_cursor_for_transcript_output_locked
|
|
53
|
+
reserve_composer_region_locked(width: width, height: height)
|
|
54
|
+
move_to_transcript_cursor_locked(width: width, height: height)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def restore_composer_cursor_locked
|
|
58
|
+
return unless @started && @asking
|
|
59
|
+
|
|
60
|
+
width, height = screen_size
|
|
61
|
+
_rows, cursor_row, cursor_col = composer_layout(width, height)
|
|
62
|
+
move_to_screen(composer_top_row(height) + cursor_row, cursor_col + 1)
|
|
63
|
+
render_cursor_visibility_locked
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def redraw_screen_locked(width: screen_width, height: screen_height)
|
|
67
|
+
return unless @started
|
|
68
|
+
|
|
69
|
+
restore_scroll_region_locked
|
|
70
|
+
@output_io.print(TTY::Cursor.clear_screen)
|
|
71
|
+
move_to_screen(1, 1)
|
|
72
|
+
@reserved_rows = 0
|
|
73
|
+
@last_composer_rows = []
|
|
74
|
+
rows, cursor_row, cursor_col = composer_layout(width, height)
|
|
75
|
+
ensure_scroll_region_locked(rows.length, redraw_transcript: false, width: width, height: height)
|
|
76
|
+
redraw_transcript_locked(width: width, height: height)
|
|
77
|
+
@rendered_rows = @asking ? rows.length : 0
|
|
78
|
+
render_composer_rows_locked(rows, height: height) if @asking
|
|
79
|
+
@cursor_rendered_row = @asking ? cursor_row : 0
|
|
80
|
+
@last_width = width
|
|
81
|
+
@last_height = height
|
|
82
|
+
reset_stream_position_from_transcript_locked(width)
|
|
83
|
+
if @asking
|
|
84
|
+
move_to_screen(composer_top_row(height) + cursor_row, cursor_col + 1)
|
|
85
|
+
render_cursor_visibility_locked
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|