kward 0.70.0 → 0.72.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 +1 -1
- data/CHANGELOG.md +89 -3
- data/Gemfile +2 -0
- data/Gemfile.lock +90 -2
- data/README.md +34 -6
- data/Rakefile +96 -0
- data/doc/agent-tools.md +52 -0
- data/doc/api.md +92 -0
- data/doc/authentication.md +58 -23
- data/doc/code-search.md +42 -2
- data/doc/configuration.md +102 -13
- data/doc/context-budgeting.md +136 -0
- data/doc/context-tools.md +83 -0
- data/doc/editor.md +394 -0
- data/doc/extensibility.md +16 -7
- data/doc/files.md +100 -0
- data/doc/getting-started.md +25 -18
- data/doc/git.md +122 -0
- data/doc/memory.md +24 -4
- data/doc/personas.md +34 -5
- data/doc/plugins.md +74 -3
- data/doc/releasing.md +45 -8
- data/doc/rpc.md +77 -15
- data/doc/session-management.md +254 -0
- data/doc/shell.md +286 -0
- data/doc/tabs.md +122 -0
- data/doc/troubleshooting.md +77 -1
- data/doc/usage.md +60 -15
- data/doc/web-search.md +12 -4
- data/doc/workspace-tools.md +144 -0
- data/examples/plugins/space_invaders.rb +377 -0
- data/lib/kward/agent.rb +1 -1
- data/lib/kward/cli/commands.rb +41 -2
- data/lib/kward/cli/git.rb +150 -0
- data/lib/kward/cli/interactive_turn.rb +73 -9
- data/lib/kward/cli/openrouter_commands.rb +55 -0
- data/lib/kward/cli/plugins.rb +54 -4
- data/lib/kward/cli/prompt_interface.rb +111 -6
- data/lib/kward/cli/rendering.rb +11 -6
- data/lib/kward/cli/runtime_helpers.rb +133 -3
- data/lib/kward/cli/sessions.rb +262 -13
- data/lib/kward/cli/settings.rb +216 -37
- data/lib/kward/cli/slash_commands.rb +439 -8
- data/lib/kward/cli/tabs.rb +695 -0
- data/lib/kward/cli.rb +171 -26
- data/lib/kward/compactor.rb +4 -1
- data/lib/kward/config_files.rb +125 -5
- data/lib/kward/context_budget_meter.rb +44 -0
- data/lib/kward/conversation.rb +59 -22
- data/lib/kward/editor_mode.rb +25 -0
- data/lib/kward/ekwsh.rb +362 -0
- data/lib/kward/model/client.rb +37 -50
- data/lib/kward/model/context_usage.rb +13 -6
- data/lib/kward/model/model_info.rb +92 -16
- data/lib/kward/model/payloads.rb +2 -0
- data/lib/kward/openrouter_model_cache.rb +120 -0
- data/lib/kward/plugin_registry.rb +108 -1
- data/lib/kward/project_files.rb +52 -0
- data/lib/kward/prompt_history.rb +82 -0
- data/lib/kward/prompt_interface/banner.rb +16 -51
- data/lib/kward/prompt_interface/composer_controller.rb +124 -83
- data/lib/kward/prompt_interface/composer_renderer.rb +116 -14
- data/lib/kward/prompt_interface/composer_state.rb +96 -27
- data/lib/kward/prompt_interface/editor/auto_close_pairs.rb +123 -0
- data/lib/kward/prompt_interface/editor/auto_indent.rb +509 -0
- data/lib/kward/prompt_interface/editor/buffer.rb +109 -0
- data/lib/kward/prompt_interface/editor/controller.rb +1018 -0
- data/lib/kward/prompt_interface/editor/endwise.rb +321 -0
- data/lib/kward/prompt_interface/editor/file_marker.rb +40 -0
- data/lib/kward/prompt_interface/editor/indent_navigation.rb +61 -0
- data/lib/kward/prompt_interface/editor/kill_ring.rb +78 -0
- data/lib/kward/prompt_interface/editor/modes/emacs.rb +259 -0
- data/lib/kward/prompt_interface/editor/modes/modern.rb +353 -0
- data/lib/kward/prompt_interface/editor/modes/vibe.rb +1962 -0
- data/lib/kward/prompt_interface/editor/renderer.rb +243 -0
- data/lib/kward/prompt_interface/editor/search.rb +76 -0
- data/lib/kward/prompt_interface/editor/selections.rb +120 -0
- data/lib/kward/prompt_interface/editor/state.rb +1249 -0
- data/lib/kward/prompt_interface/editor/status_text.rb +23 -0
- data/lib/kward/prompt_interface/editor/syntax_highlighter.rb +420 -0
- data/lib/kward/prompt_interface/editor/undo_history.rb +46 -0
- data/lib/kward/prompt_interface/editor/vibe_state.rb +44 -0
- data/lib/kward/prompt_interface/file_overlay.rb +211 -0
- data/lib/kward/prompt_interface/git_prompt.rb +299 -0
- data/lib/kward/prompt_interface/interactive/controller.rb +186 -0
- data/lib/kward/prompt_interface/interactive/renderer.rb +71 -0
- data/lib/kward/prompt_interface/interactive/state.rb +62 -0
- data/lib/kward/prompt_interface/key_handler.rb +416 -43
- data/lib/kward/prompt_interface/layout.rb +2 -2
- data/lib/kward/prompt_interface/overlay_renderer.rb +21 -2
- data/lib/kward/prompt_interface/project_browser.rb +524 -0
- data/lib/kward/prompt_interface/prompt_renderer.rb +32 -13
- data/lib/kward/prompt_interface/question_prompt.rb +122 -82
- data/lib/kward/prompt_interface/runtime_state.rb +49 -1
- data/lib/kward/prompt_interface/screen.rb +17 -0
- data/lib/kward/prompt_interface/selection_prompt.rb +511 -58
- data/lib/kward/prompt_interface/stream_state.rb +7 -0
- data/lib/kward/prompt_interface/transcript_buffer.rb +13 -16
- data/lib/kward/prompt_interface/transcript_renderer.rb +3 -3
- data/lib/kward/prompt_interface.rb +307 -35
- data/lib/kward/prompts/commands.rb +7 -1
- data/lib/kward/prompts.rb +4 -2
- data/lib/kward/rpc/server.rb +45 -11
- data/lib/kward/rpc/session_manager.rb +52 -53
- data/lib/kward/rpc/session_tree_rows.rb +9 -115
- data/lib/kward/rpc/tool_event_normalizer.rb +1 -1
- data/lib/kward/session_store.rb +67 -4
- data/lib/kward/session_tree_nodes.rb +136 -0
- data/lib/kward/session_tree_renderer.rb +9 -131
- data/lib/kward/tab_store.rb +47 -0
- data/lib/kward/telemetry/logger.rb +5 -3
- data/lib/kward/text_boundary.rb +25 -0
- data/lib/kward/tool_output_compactor.rb +127 -0
- data/lib/kward/tools/base.rb +8 -2
- data/lib/kward/tools/context_budget_stats.rb +54 -0
- data/lib/kward/tools/context_for_task.rb +202 -0
- data/lib/kward/tools/read_file.rb +8 -4
- data/lib/kward/tools/registry.rb +92 -15
- data/lib/kward/tools/retrieve_tool_output.rb +71 -0
- data/lib/kward/tools/search/web.rb +2 -2
- data/lib/kward/tools/summarize_file_structure.rb +29 -0
- data/lib/kward/tools/tool_call.rb +12 -0
- data/lib/kward/version.rb +1 -1
- data/lib/kward/workers/git_guard.rb +68 -0
- data/lib/kward/workers/live_view.rb +49 -0
- data/lib/kward/workers/manager.rb +288 -0
- data/lib/kward/workers/store.rb +72 -0
- data/lib/kward/workers/tool_policy.rb +23 -0
- data/lib/kward/workers/worker.rb +82 -0
- data/lib/kward/workers/write_lock.rb +38 -0
- data/lib/kward/workers.rb +7 -0
- data/lib/kward/workspace.rb +154 -12
- data/templates/default/fulldoc/html/css/kward.css +362 -42
- data/templates/default/fulldoc/html/full_list.erb +107 -0
- data/templates/default/fulldoc/html/js/kward.js +161 -2
- data/templates/default/fulldoc/html/setup.rb +8 -0
- data/templates/default/kward_navigation.rb +102 -0
- data/templates/default/layout/html/layout.erb +43 -10
- data/templates/default/layout/html/setup.rb +39 -38
- metadata +65 -3
- data/lib/kward/resources/avatar_kward_logo.rb +0 -50
- data/lib/kward/resources/pixel_logo.rb +0 -232
|
@@ -8,13 +8,7 @@ module Kward
|
|
|
8
8
|
|
|
9
9
|
def ask_single_user_question(question, index, total)
|
|
10
10
|
@mutex.synchronize do
|
|
11
|
-
|
|
12
|
-
self.composer_input = ""
|
|
13
|
-
self.composer_cursor = 0
|
|
14
|
-
@pending_keys.clear
|
|
15
|
-
@asking = true
|
|
16
|
-
@busy = false
|
|
17
|
-
@queued_count = 0
|
|
11
|
+
prepare_modal_input_locked("Answer>")
|
|
18
12
|
@question_state = {
|
|
19
13
|
question: question[:question] || question["question"],
|
|
20
14
|
header: question[:header] || question["header"],
|
|
@@ -23,7 +17,6 @@ module Kward
|
|
|
23
17
|
index: index,
|
|
24
18
|
total: total
|
|
25
19
|
}
|
|
26
|
-
reset_history_navigation
|
|
27
20
|
render_prompt_locked
|
|
28
21
|
end
|
|
29
22
|
|
|
@@ -37,6 +30,7 @@ module Kward
|
|
|
37
30
|
render_prompt_locked if resized || footer_refreshed
|
|
38
31
|
else
|
|
39
32
|
result = handle_question_key(key)
|
|
33
|
+
result = drain_pending_question_keys_locked(result)
|
|
40
34
|
render_prompt_locked unless result.is_a?(Hash) || result == SELECT_CANCEL
|
|
41
35
|
end
|
|
42
36
|
end
|
|
@@ -47,6 +41,13 @@ module Kward
|
|
|
47
41
|
end
|
|
48
42
|
end
|
|
49
43
|
|
|
44
|
+
def drain_pending_question_keys_locked(result)
|
|
45
|
+
until result.is_a?(Hash) || result == SELECT_CANCEL || @pending_keys.empty?
|
|
46
|
+
result = handle_question_key(@pending_keys.shift)
|
|
47
|
+
end
|
|
48
|
+
result
|
|
49
|
+
end
|
|
50
|
+
|
|
50
51
|
def begin_question_prompt_state
|
|
51
52
|
{
|
|
52
53
|
prompt_label: @prompt_label,
|
|
@@ -64,6 +65,7 @@ module Kward
|
|
|
64
65
|
def finish_question_prompt(saved_state)
|
|
65
66
|
@mutex.synchronize do
|
|
66
67
|
@question_state = nil
|
|
68
|
+
@question_prompt_active = false
|
|
67
69
|
@select_state = saved_state[:select_state]
|
|
68
70
|
@prompt_label = saved_state[:prompt_label]
|
|
69
71
|
self.composer_input = saved_state[:input]
|
|
@@ -80,20 +82,17 @@ module Kward
|
|
|
80
82
|
|
|
81
83
|
def handle_question_key(key)
|
|
82
84
|
return if handle_question_bracketed_paste_key(key)
|
|
85
|
+
return if handle_question_shift_enter_key(key)
|
|
83
86
|
|
|
84
87
|
csi_result = handle_question_csi_u_key(key)
|
|
85
88
|
return csi_result unless csi_result == false
|
|
86
89
|
|
|
87
|
-
if key
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
return handle_question_key(token)
|
|
92
|
-
end
|
|
93
|
-
end
|
|
90
|
+
return true if handle_bundled_key(key) { |token| handle_question_key(token) }
|
|
91
|
+
|
|
92
|
+
binding_result = handle_question_composer_key_binding(key)
|
|
93
|
+
return binding_result unless binding_result == false
|
|
94
94
|
|
|
95
|
-
|
|
96
|
-
case key_name
|
|
95
|
+
case key_name_for(key)
|
|
97
96
|
when :return, :enter
|
|
98
97
|
current_question_answer
|
|
99
98
|
when :backspace
|
|
@@ -101,13 +100,13 @@ module Kward
|
|
|
101
100
|
when :delete
|
|
102
101
|
question_delete_at_cursor
|
|
103
102
|
when :left
|
|
104
|
-
|
|
103
|
+
move_cursor_left
|
|
105
104
|
when :right
|
|
106
|
-
|
|
105
|
+
move_cursor_right
|
|
107
106
|
when :home
|
|
108
|
-
|
|
107
|
+
move_to_start_of_line
|
|
109
108
|
when :end
|
|
110
|
-
|
|
109
|
+
move_to_end_of_line
|
|
111
110
|
when :up
|
|
112
111
|
question_previous_choice
|
|
113
112
|
when :down
|
|
@@ -127,59 +126,80 @@ module Kward
|
|
|
127
126
|
end
|
|
128
127
|
|
|
129
128
|
def handle_question_csi_u_key(key)
|
|
130
|
-
|
|
131
|
-
return false unless
|
|
129
|
+
sequence = parse_csi_u_key(key)
|
|
130
|
+
return false unless sequence
|
|
132
131
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
queue_pending_keys(
|
|
132
|
+
code = sequence[:code]
|
|
133
|
+
modifier = sequence[:modifier]
|
|
134
|
+
queue_pending_keys(sequence[:remaining]) if sequence[:remaining] && !sequence[:remaining].empty?
|
|
136
135
|
|
|
137
136
|
case code
|
|
138
137
|
when 13
|
|
139
|
-
|
|
138
|
+
if modifier == 2
|
|
139
|
+
question_insert_string("\n")
|
|
140
|
+
nil
|
|
141
|
+
else
|
|
142
|
+
current_question_answer
|
|
143
|
+
end
|
|
140
144
|
when 27
|
|
141
145
|
SELECT_CANCEL
|
|
142
146
|
when 8, 127
|
|
143
|
-
question_delete_before_cursor
|
|
147
|
+
alt_modifier?(modifier) ? question_delete_word_before_cursor : question_delete_before_cursor
|
|
144
148
|
nil
|
|
145
149
|
else
|
|
146
|
-
|
|
150
|
+
modified_result = handle_question_modified_csi_u_key(code, modifier)
|
|
151
|
+
return modified_result unless modified_result == false
|
|
152
|
+
|
|
153
|
+
question_insert_csi_u_text(sequence)
|
|
147
154
|
end
|
|
148
155
|
end
|
|
149
156
|
|
|
157
|
+
def handle_question_modified_csi_u_key(code, modifier)
|
|
158
|
+
before = composer_input.dup
|
|
159
|
+
result = handle_modified_csi_u_key(code, modifier)
|
|
160
|
+
question_select_custom_choice if result != false && composer_input != before
|
|
161
|
+
result
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def question_insert_csi_u_text(sequence)
|
|
165
|
+
text = csi_u_printable_text(sequence)
|
|
166
|
+
return true if text.nil? && csi_u_text_field?(sequence)
|
|
167
|
+
return false unless text
|
|
168
|
+
|
|
169
|
+
question_insert_string(text)
|
|
170
|
+
end
|
|
171
|
+
|
|
150
172
|
def handle_question_escape_sequence
|
|
151
|
-
|
|
152
|
-
return SELECT_CANCEL if
|
|
173
|
+
pending_sequence = read_pending_escape_sequence
|
|
174
|
+
return SELECT_CANCEL if pending_sequence.empty?
|
|
153
175
|
|
|
154
|
-
|
|
155
|
-
|
|
176
|
+
full_sequence = "\e#{pending_sequence}"
|
|
177
|
+
sequence = next_key_token(full_sequence)
|
|
178
|
+
queue_pending_keys(full_sequence[sequence.length..]) if full_sequence.length > sequence.length
|
|
179
|
+
return SELECT_CANCEL if sequence == "\e"
|
|
180
|
+
|
|
181
|
+
binding_result = handle_question_composer_key_binding(sequence)
|
|
182
|
+
return binding_result unless binding_result == false
|
|
183
|
+
|
|
184
|
+
case key_name_for(sequence)
|
|
156
185
|
when :up
|
|
157
186
|
question_previous_choice
|
|
158
187
|
when :down
|
|
159
188
|
question_next_choice
|
|
160
189
|
when :left
|
|
161
|
-
|
|
190
|
+
move_cursor_left
|
|
162
191
|
when :right
|
|
163
|
-
|
|
192
|
+
move_cursor_right
|
|
164
193
|
end
|
|
165
194
|
true
|
|
166
195
|
end
|
|
167
196
|
|
|
168
197
|
def handle_question_bracketed_paste_key(key)
|
|
169
|
-
|
|
170
|
-
return false unless
|
|
171
|
-
|
|
172
|
-
pasted = text[BRACKETED_PASTE_START.length..] || ""
|
|
173
|
-
until pasted.include?(BRACKETED_PASTE_END)
|
|
174
|
-
chunk = @reader.read_keypress(echo: false, raw: true)
|
|
175
|
-
break if chunk.nil?
|
|
198
|
+
paste = read_bracketed_paste(key)
|
|
199
|
+
return false unless paste
|
|
176
200
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
content, remaining = pasted.split(BRACKETED_PASTE_END, 2)
|
|
181
|
-
question_insert_string(normalize_paste(content || ""))
|
|
182
|
-
queue_pending_keys(remaining) if remaining && !remaining.empty?
|
|
201
|
+
question_insert_string(normalize_paste(paste[:content]))
|
|
202
|
+
queue_pending_keys(paste[:remaining]) if paste[:remaining] && !paste[:remaining].empty?
|
|
183
203
|
true
|
|
184
204
|
end
|
|
185
205
|
|
|
@@ -248,38 +268,78 @@ module Kward
|
|
|
248
268
|
question_insert_string(key)
|
|
249
269
|
end
|
|
250
270
|
|
|
271
|
+
def handle_question_shift_enter_key(key)
|
|
272
|
+
sequence = shift_enter_sequence_for(key)
|
|
273
|
+
return false unless sequence
|
|
274
|
+
|
|
275
|
+
question_insert_string("\n")
|
|
276
|
+
queue_pending_keys(key[sequence.length..]) if key.length > sequence.length
|
|
277
|
+
true
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def handle_question_composer_key_binding(key)
|
|
281
|
+
before = composer_input.dup
|
|
282
|
+
result = handle_composer_key_binding(key)
|
|
283
|
+
question_select_custom_choice if result != false && composer_input != before
|
|
284
|
+
result
|
|
285
|
+
end
|
|
286
|
+
|
|
251
287
|
def question_insert_string(string)
|
|
252
288
|
return if string.empty?
|
|
253
289
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
@question_state[:selection_index] = question_choices.length - 1 if @question_state
|
|
290
|
+
insert_string(string)
|
|
291
|
+
question_select_custom_choice
|
|
257
292
|
end
|
|
258
293
|
|
|
259
294
|
def question_delete_before_cursor
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
self.composer_cursor -= 1
|
|
264
|
-
@question_state[:selection_index] = question_choices.length - 1 if @question_state && !composer_input.empty?
|
|
295
|
+
before = composer_input.dup
|
|
296
|
+
delete_before_cursor
|
|
297
|
+
question_select_custom_choice if composer_input != before && !composer_input.empty?
|
|
265
298
|
end
|
|
266
299
|
|
|
267
300
|
def question_delete_at_cursor
|
|
268
|
-
|
|
301
|
+
before = composer_input.dup
|
|
302
|
+
delete_at_cursor
|
|
303
|
+
question_select_custom_choice if composer_input != before && !composer_input.empty?
|
|
304
|
+
end
|
|
269
305
|
|
|
270
|
-
|
|
271
|
-
|
|
306
|
+
def question_delete_word_before_cursor
|
|
307
|
+
before = composer_input.dup
|
|
308
|
+
delete_word_before_cursor
|
|
309
|
+
question_select_custom_choice if composer_input != before && !composer_input.empty?
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def question_select_custom_choice
|
|
313
|
+
@question_state[:selection_index] = question_choices.length - 1 if @question_state
|
|
272
314
|
end
|
|
273
315
|
|
|
274
316
|
def question_composer_layout(width, height = screen_height)
|
|
275
317
|
content_width = [width - 4, 1].max
|
|
276
318
|
overlay_rows = active_overlay_rows(width, height: height)
|
|
277
|
-
|
|
278
|
-
return [rows, question_custom_cursor_row, question_custom_cursor_col(width)] if selected_question_choice&.fetch(:custom, false)
|
|
319
|
+
return question_custom_composer_layout(width, height, overlay_rows, content_width) if selected_question_choice&.fetch(:custom, false)
|
|
279
320
|
|
|
321
|
+
rows = overlay_rows + [top_border(width), box_content_row("", content_width)]
|
|
322
|
+
rows.concat(question_bottom_border_rows(width))
|
|
280
323
|
[rows, overlay_rows.length + 1, 2]
|
|
281
324
|
end
|
|
282
325
|
|
|
326
|
+
def question_custom_composer_layout(width, height, overlay_rows, content_width)
|
|
327
|
+
input_layout_rows, input_cursor_row, input_cursor_col = input_layout(content_width)
|
|
328
|
+
max_input_rows = max_visible_input_rows(0, overlay_rows.length, 0, height: height)
|
|
329
|
+
visible_start = [[input_cursor_row - max_input_rows + 1, 0].max, [input_layout_rows.length - max_input_rows, 0].max].min
|
|
330
|
+
visible_rows = input_layout_rows[visible_start, max_input_rows] || [""]
|
|
331
|
+
rows = overlay_rows + [top_border(width)]
|
|
332
|
+
rows.concat(visible_rows.map { |row| box_content_row(row, content_width) })
|
|
333
|
+
rows.concat(question_bottom_border_rows(width))
|
|
334
|
+
cursor_row = overlay_rows.length + 1 + input_cursor_row - visible_start
|
|
335
|
+
cursor_col = 2 + [input_cursor_col, content_width - 1].min
|
|
336
|
+
[rows, cursor_row, cursor_col]
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def question_bottom_border_rows(width)
|
|
340
|
+
@tabs.empty? ? [bottom_border(width)] : tab_border_rows(width)
|
|
341
|
+
end
|
|
342
|
+
|
|
283
343
|
def question_overlay_rows(width)
|
|
284
344
|
title = "Question #{@question_state[:index]}/#{@question_state[:total]} · #{@question_state[:header]}"
|
|
285
345
|
lines = [
|
|
@@ -294,35 +354,15 @@ module Kward
|
|
|
294
354
|
overlay_card_rows(title, lines, width)
|
|
295
355
|
end
|
|
296
356
|
|
|
297
|
-
def question_custom_cursor_row
|
|
298
|
-
4 + question_choices.index { |choice| choice[:custom] }.to_i
|
|
299
|
-
end
|
|
300
|
-
|
|
301
|
-
def question_custom_cursor_col(width)
|
|
302
|
-
card_width = overlay_card_width(width)
|
|
303
|
-
left_padding = overlay_left_padding(width, card_width)
|
|
304
|
-
custom_prefix = selected_question_choice&.fetch(:custom, false) || !composer_input.empty? ? "Type something: " : "Type something."
|
|
305
|
-
visible_before_cursor = display_question_input(composer_input[0...composer_cursor])
|
|
306
|
-
[[left_padding + 2 + 2 + custom_prefix.length + visible_before_cursor.length, width - 1].min, 0].max
|
|
307
|
-
end
|
|
308
|
-
|
|
309
357
|
def choice_text(choice, selected: false)
|
|
310
358
|
if choice[:custom]
|
|
311
|
-
|
|
312
|
-
"Type something: #{display_question_input(composer_input)}"
|
|
313
|
-
else
|
|
314
|
-
"Type something."
|
|
315
|
-
end
|
|
359
|
+
selected ? "Type a custom answer below." : "Type something."
|
|
316
360
|
else
|
|
317
361
|
description = choice[:description].empty? ? "" : " — #{choice[:description]}"
|
|
318
362
|
"#{choice[:label]}#{description}"
|
|
319
363
|
end
|
|
320
364
|
end
|
|
321
365
|
|
|
322
|
-
def display_question_input(value)
|
|
323
|
-
value.to_s.gsub(/\s+/, " ")
|
|
324
|
-
end
|
|
325
|
-
|
|
326
366
|
end
|
|
327
367
|
end
|
|
328
368
|
end
|
|
@@ -17,7 +17,7 @@ module Kward
|
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
def tick_spinner_locked
|
|
20
|
-
return false unless
|
|
20
|
+
return false unless spinner_active?
|
|
21
21
|
|
|
22
22
|
now = monotonic_now
|
|
23
23
|
elapsed = now - @last_spinner_tick
|
|
@@ -33,6 +33,11 @@ module Kward
|
|
|
33
33
|
SPINNER_FRAMES[@spinner_frame_index % SPINNER_FRAMES.length]
|
|
34
34
|
end
|
|
35
35
|
|
|
36
|
+
def spinner_active?
|
|
37
|
+
busy = @busy || (@select_state && @select_state[:busy_activity])
|
|
38
|
+
busy && @queued_count.zero? && @started && @asking
|
|
39
|
+
end
|
|
40
|
+
|
|
36
41
|
def tick_footer_locked
|
|
37
42
|
return false unless @footer && @started && @asking
|
|
38
43
|
|
|
@@ -45,6 +50,21 @@ module Kward
|
|
|
45
50
|
true
|
|
46
51
|
end
|
|
47
52
|
|
|
53
|
+
def cached_composer_status_text
|
|
54
|
+
return nil unless @composer_status
|
|
55
|
+
|
|
56
|
+
now = monotonic_now
|
|
57
|
+
elapsed = now - @last_composer_status_refresh.to_f
|
|
58
|
+
if @cached_composer_status_text.nil? || elapsed >= COMPOSER_STATUS_REFRESH_INTERVAL
|
|
59
|
+
text = @composer_status.call.to_s
|
|
60
|
+
@cached_composer_status_text = text.empty? ? nil : status_composer_text(text)
|
|
61
|
+
@last_composer_status_refresh = now
|
|
62
|
+
end
|
|
63
|
+
@cached_composer_status_text
|
|
64
|
+
rescue StandardError
|
|
65
|
+
@cached_composer_status_text = nil
|
|
66
|
+
end
|
|
67
|
+
|
|
48
68
|
def monotonic_now
|
|
49
69
|
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
50
70
|
end
|
|
@@ -54,6 +74,34 @@ module Kward
|
|
|
54
74
|
ANSI.colorize(text, *styles, enabled: @color_enabled)
|
|
55
75
|
end
|
|
56
76
|
|
|
77
|
+
def normalize_tab_keybindings(value)
|
|
78
|
+
text = value.to_s.downcase
|
|
79
|
+
return "ctrl" if text == "ctrl"
|
|
80
|
+
return "alt" if text == "alt"
|
|
81
|
+
|
|
82
|
+
RbConfig::CONFIG["host_os"].to_s.downcase.include?("darwin") ? "ctrl" : "alt"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def normalize_editor_mode(value)
|
|
86
|
+
EditorMode.normalize(value)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def normalize_editor_line_numbers(value)
|
|
90
|
+
EditorMode.normalize_line_numbers(value)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def tab_action_result?(result)
|
|
94
|
+
result.is_a?(Hash) && result[:tab_action]
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def reasoning_action_result?(result)
|
|
98
|
+
result.is_a?(Hash) && result[:reasoning_action]
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def prompt_action_result?(result)
|
|
102
|
+
tab_action_result?(result) || reasoning_action_result?(result)
|
|
103
|
+
end
|
|
104
|
+
|
|
57
105
|
end
|
|
58
106
|
end
|
|
59
107
|
end
|
|
@@ -55,6 +55,9 @@ module Kward
|
|
|
55
55
|
|
|
56
56
|
def render_cursor_visibility_locked
|
|
57
57
|
visible = !(@question_state && !selected_question_choice&.fetch(:custom, false))
|
|
58
|
+
visible = select_editing_active? if @select_state
|
|
59
|
+
visible = git_composing? if @git_state
|
|
60
|
+
visible = project_browser_search_active? if project_browser_visible?
|
|
58
61
|
set_cursor_visible_locked(visible)
|
|
59
62
|
end
|
|
60
63
|
|
|
@@ -65,6 +68,20 @@ module Kward
|
|
|
65
68
|
@cursor_visible = visible
|
|
66
69
|
end
|
|
67
70
|
|
|
71
|
+
def set_editor_bar_cursor_locked
|
|
72
|
+
return if @editor_bar_cursor_active
|
|
73
|
+
|
|
74
|
+
@output_io.print(CURSOR_SHAPE_BAR)
|
|
75
|
+
@editor_bar_cursor_active = true
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def restore_editor_cursor_shape_locked
|
|
79
|
+
return unless @editor_bar_cursor_active
|
|
80
|
+
|
|
81
|
+
@output_io.print(CURSOR_SHAPE_DEFAULT)
|
|
82
|
+
@editor_bar_cursor_active = false
|
|
83
|
+
end
|
|
84
|
+
|
|
68
85
|
def reserve_composer_region_locked(width: screen_width, height: screen_height)
|
|
69
86
|
rows, = composer_layout(width, height)
|
|
70
87
|
ensure_scroll_region_locked(rows.length, width: width, height: height)
|