kward 0.82.0 → 0.84.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 +96 -15
- data/CONTRIBUTING.md +74 -0
- data/Gemfile.lock +8 -2
- data/README.md +21 -1
- data/Rakefile +46 -2
- data/SECURITY.md +31 -0
- data/doc/agent-tools.md +1 -0
- data/doc/api.md +4 -0
- data/doc/composer.md +2 -2
- data/doc/configuration.md +82 -20
- data/doc/editor.md +47 -22
- data/doc/files.md +12 -7
- data/doc/getting-started.md +3 -0
- data/doc/pan.md +19 -15
- data/doc/permissions.md +1 -0
- data/doc/platform-support.md +48 -0
- data/doc/releasing.md +12 -4
- data/doc/rpc.md +2 -2
- data/doc/sandboxing.md +5 -1
- data/doc/security.md +4 -3
- data/doc/shell.md +101 -60
- data/doc/tabs.md +3 -0
- data/doc/troubleshooting.md +12 -2
- data/doc/usage.md +7 -6
- data/kward.gemspec +5 -4
- data/lib/kward/agent.rb +7 -3
- data/lib/kward/ansi.rb +110 -10
- data/lib/kward/auth/anthropic_oauth.rb +7 -7
- data/lib/kward/cli/auth_commands.rb +34 -13
- data/lib/kward/cli/commands.rb +83 -62
- data/lib/kward/cli/doctor.rb +39 -17
- data/lib/kward/cli/hook_commands.rb +22 -12
- data/lib/kward/cli/interactive_turn.rb +144 -21
- data/lib/kward/cli/project_skills_commands.rb +8 -4
- data/lib/kward/cli/prompt_interface.rb +27 -0
- data/lib/kward/cli/rendering.rb +15 -9
- data/lib/kward/cli/runtime_helpers.rb +260 -55
- data/lib/kward/cli/slash_commands.rb +21 -12
- data/lib/kward/cli/tabs.rb +91 -13
- data/lib/kward/cli/tool_summaries.rb +14 -0
- data/lib/kward/cli.rb +55 -7
- data/lib/kward/cli_transcript_formatter.rb +11 -4
- data/lib/kward/compaction/token_estimator.rb +12 -6
- data/lib/kward/config_files.rb +94 -63
- data/lib/kward/detached_run.rb +44 -0
- data/lib/kward/editor_prompt.rb +46 -0
- data/lib/kward/editor_prompt_session.rb +28 -0
- data/lib/kward/interactive_pty_runner.rb +102 -28
- data/lib/kward/{ekwsh.rb → kwsh.rb} +103 -24
- data/lib/kward/kwshrc.rb +233 -0
- data/lib/kward/markdown_code_block.rb +136 -0
- data/lib/kward/model/client.rb +35 -39
- data/lib/kward/model/model_info.rb +3 -2
- data/lib/kward/model/payloads.rb +0 -2
- data/lib/kward/model/provider_catalog.rb +5 -0
- data/lib/kward/model/stream_parser.rb +20 -4
- data/lib/kward/pan/index.html.erb +3 -3
- data/lib/kward/pan/server.rb +23 -3
- data/lib/kward/persistent_shell_session.rb +843 -0
- data/lib/kward/project_files.rb +19 -6
- data/lib/kward/prompt_interface/composer_renderer.rb +45 -41
- data/lib/kward/prompt_interface/composer_state.rb +33 -24
- data/lib/kward/prompt_interface/editor/auto_indent.rb +27 -22
- data/lib/kward/prompt_interface/editor/controller.rb +99 -33
- data/lib/kward/prompt_interface/editor/endwise.rb +13 -4
- data/lib/kward/prompt_interface/editor/modes/modern.rb +4 -0
- data/lib/kward/prompt_interface/editor/modes/vibe.rb +298 -47
- data/lib/kward/prompt_interface/editor/renderer.rb +109 -6
- data/lib/kward/prompt_interface/editor/runner.rb +362 -0
- data/lib/kward/prompt_interface/editor/runner_state.rb +78 -0
- data/lib/kward/prompt_interface/editor/state.rb +21 -10
- data/lib/kward/prompt_interface/editor/syntax_highlighter.rb +68 -6
- data/lib/kward/prompt_interface/editor/vibe_state.rb +3 -3
- data/lib/kward/prompt_interface/editor/word_completion.rb +124 -0
- data/lib/kward/prompt_interface/file_overlay.rb +92 -22
- data/lib/kward/prompt_interface/key_handler.rb +75 -0
- data/lib/kward/prompt_interface/overlay_renderer.rb +7 -5
- data/lib/kward/prompt_interface/project_browser.rb +443 -17
- data/lib/kward/prompt_interface/runtime_state.rb +55 -2
- data/lib/kward/prompt_interface/screen.rb +2 -2
- data/lib/kward/prompt_interface/selection_prompt.rb +3 -1
- data/lib/kward/prompt_interface/slash_overlay.rb +19 -4
- data/lib/kward/prompt_interface/transcript_renderer.rb +12 -7
- data/lib/kward/prompt_interface.rb +169 -18
- data/lib/kward/prompts/commands.rb +2 -1
- data/lib/kward/prompts.rb +1 -1
- data/lib/kward/pty_output_sink.rb +47 -0
- data/lib/kward/rpc/auth_manager.rb +1 -1
- data/lib/kward/rpc/server.rb +2 -1
- data/lib/kward/rpc/transcript_normalizer.rb +7 -3
- data/lib/kward/scratchpad_languages.rb +74 -0
- data/lib/kward/scratchpad_runner.rb +155 -29
- data/lib/kward/shell_prompt.rb +52 -0
- data/lib/kward/shell_prompt_session.rb +58 -0
- data/lib/kward/terminal_keys.rb +13 -0
- data/lib/kward/terminal_text.rb +121 -0
- data/lib/kward/text_matcher.rb +18 -0
- data/lib/kward/tools/open_editor.rb +41 -0
- data/lib/kward/tools/prepare_shell_command.rb +28 -0
- data/lib/kward/tools/registry.rb +75 -8
- data/lib/kward/tools/replace_editor_buffer.rb +30 -0
- data/lib/kward/tools/run_shell_command.rb +24 -6
- data/lib/kward/tools/tool_call.rb +2 -1
- data/lib/kward/version.rb +1 -1
- data/templates/default/fulldoc/html/css/kward.css +0 -125
- data/templates/default/fulldoc/html/images/kward_workflow.svg +52 -0
- data/templates/default/fulldoc/html/setup.rb +1 -1
- data/templates/default/kward_navigation.rb +1 -0
- data/templates/default/layout/html/footer.erb +10 -0
- data/templates/default/layout/html/headers.erb +23 -0
- data/templates/default/layout/html/layout.erb +6 -18
- data/templates/default/layout/html/setup.rb +41 -2
- metadata +44 -8
- data/templates/default/fulldoc/html/images/kward_screen_1.png +0 -0
|
@@ -17,6 +17,9 @@ module Kward
|
|
|
17
17
|
VIBE_RUBY_BLOCK_OPENERS = %w[if unless case while until for def module class do begin].freeze
|
|
18
18
|
VIBE_RUBY_PATHS = %w[Gemfile Rakefile Guardfile Capfile Vagrantfile].freeze
|
|
19
19
|
VIBE_RUBY_EXTENSIONS = %w[.rb .rake .gemspec].freeze
|
|
20
|
+
VIBE_VISUAL_LINE_MOVE_CONTROLS = {
|
|
21
|
+
TerminalKeys::CTRL_H => "h", TerminalKeys::CTRL_J => "j", TerminalKeys::CTRL_K => "k", TerminalKeys::CTRL_L => "l"
|
|
22
|
+
}.freeze
|
|
20
23
|
|
|
21
24
|
VibeOperatorTarget = Struct.new(:type, :start_index, :end_index, :replacement_text, :replacement_cursor_offset, keyword_init: true) do
|
|
22
25
|
def characterwise?
|
|
@@ -38,6 +41,9 @@ module Kward
|
|
|
38
41
|
csi_result = handle_vibe_csi_u_key(key)
|
|
39
42
|
return csi_result unless csi_result == false
|
|
40
43
|
|
|
44
|
+
multi_cursor_result = handle_vibe_multi_cursor_key(key)
|
|
45
|
+
return multi_cursor_result unless multi_cursor_result == false
|
|
46
|
+
|
|
41
47
|
return handle_vibe_command_key(key) if @editor_state.vibe_mode == "command"
|
|
42
48
|
|
|
43
49
|
tab_result = handle_tab_key_binding(key)
|
|
@@ -84,6 +90,7 @@ module Kward
|
|
|
84
90
|
|
|
85
91
|
@editor_state.vibe_command = ""
|
|
86
92
|
@editor_state.vibe_pending = ""
|
|
93
|
+
@editor_state.vibe_command_range = nil
|
|
87
94
|
@editor_state.clear_selection
|
|
88
95
|
vibe_return_to_normal
|
|
89
96
|
end
|
|
@@ -123,6 +130,35 @@ module Kward
|
|
|
123
130
|
nil
|
|
124
131
|
end
|
|
125
132
|
|
|
133
|
+
def handle_vibe_multi_cursor_key(key)
|
|
134
|
+
return false unless key == TerminalKeys::CTRL_D
|
|
135
|
+
return false if editor_search_active?
|
|
136
|
+
|
|
137
|
+
visual_selection = @editor_state.vibe_mode == "visual"
|
|
138
|
+
return false unless visual_selection || %w[normal insert replace].include?(@editor_state.vibe_mode)
|
|
139
|
+
|
|
140
|
+
original_selection = vibe_prepare_visual_selection_for_multi_cursor if visual_selection
|
|
141
|
+
added = @editor_state.add_next_occurrence_selection
|
|
142
|
+
if added
|
|
143
|
+
vibe_enter_insert_mode(TerminalKeys::CTRL_D) unless @editor_state.vibe_mode == "insert"
|
|
144
|
+
elsif original_selection
|
|
145
|
+
@editor_state.vibe_mode = "visual"
|
|
146
|
+
@editor_state.set_selections([original_selection])
|
|
147
|
+
end
|
|
148
|
+
true
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def vibe_prepare_visual_selection_for_multi_cursor
|
|
152
|
+
range = @editor_state.selection_range
|
|
153
|
+
return unless range
|
|
154
|
+
|
|
155
|
+
original_selection = { anchor: @editor_state.selection_anchor, cursor: @editor_state.cursor }
|
|
156
|
+
vibe_remember_visual_selection
|
|
157
|
+
@editor_state.vibe_mode = "normal"
|
|
158
|
+
@editor_state.set_selections([{ anchor: range[0], cursor: range[1] }])
|
|
159
|
+
original_selection
|
|
160
|
+
end
|
|
161
|
+
|
|
126
162
|
def handle_vibe_search_key(key)
|
|
127
163
|
case key
|
|
128
164
|
when "\n", "\r"
|
|
@@ -159,7 +195,7 @@ module Kward
|
|
|
159
195
|
named_result = handle_vibe_insert_named_key(key_name) if key_name
|
|
160
196
|
return named_result unless named_result == false || named_result.nil?
|
|
161
197
|
|
|
162
|
-
vibe_record_undo {
|
|
198
|
+
vibe_record_undo { vibe_insert_printable(key) } if printable_key?(key)
|
|
163
199
|
end
|
|
164
200
|
end
|
|
165
201
|
|
|
@@ -186,15 +222,23 @@ module Kward
|
|
|
186
222
|
end
|
|
187
223
|
end
|
|
188
224
|
|
|
225
|
+
def vibe_insert_printable(text)
|
|
226
|
+
return editor_insert_printable(text) unless @editor_state.multi_cursor?
|
|
227
|
+
|
|
228
|
+
@editor_state.replace_selections(text)
|
|
229
|
+
end
|
|
230
|
+
|
|
189
231
|
def vibe_replace_character(key)
|
|
190
232
|
@editor_state.delete_at_cursor
|
|
191
|
-
|
|
233
|
+
vibe_insert_printable(key)
|
|
192
234
|
end
|
|
193
235
|
|
|
194
236
|
def handle_vibe_command_key(key)
|
|
237
|
+
result = nil
|
|
195
238
|
case key
|
|
196
239
|
when "\e", TerminalKeys::CTRL_C, :escape
|
|
197
240
|
@editor_state.vibe_command = ""
|
|
241
|
+
@editor_state.vibe_command_range = nil
|
|
198
242
|
vibe_return_to_normal
|
|
199
243
|
when "\b", "\x7F"
|
|
200
244
|
@editor_state.vibe_command = @editor_state.vibe_command[0...-1].to_s
|
|
@@ -202,20 +246,25 @@ module Kward
|
|
|
202
246
|
when "\t"
|
|
203
247
|
vibe_complete_command_path
|
|
204
248
|
when "\n", "\r"
|
|
205
|
-
execute_vibe_command(@editor_state.vibe_command)
|
|
249
|
+
result = execute_vibe_command(@editor_state.vibe_command)
|
|
206
250
|
else
|
|
207
251
|
if printable_key?(key)
|
|
208
252
|
@editor_state.vibe_command = @editor_state.vibe_command.to_s + key
|
|
209
253
|
@editor_state.status = ":#{@editor_state.vibe_command}"
|
|
210
254
|
end
|
|
211
255
|
end
|
|
212
|
-
true
|
|
256
|
+
result || true
|
|
213
257
|
end
|
|
214
258
|
|
|
215
259
|
def execute_vibe_command(command)
|
|
216
260
|
command = command.to_s.strip
|
|
261
|
+
command_range = @editor_state.vibe_command_range
|
|
262
|
+
command_range ||= vibe_visual_line_bounds if vibe_visual_mode?
|
|
263
|
+
command_range, command = vibe_extract_command_range(command, command_range)
|
|
264
|
+
@editor_state.vibe_command_range = nil
|
|
217
265
|
@editor_state.vibe_mode = "normal"
|
|
218
266
|
@editor_state.vibe_command = ""
|
|
267
|
+
action = nil
|
|
219
268
|
case command
|
|
220
269
|
when "w"
|
|
221
270
|
save_editor
|
|
@@ -223,8 +272,19 @@ module Kward
|
|
|
223
272
|
save_editor(Regexp.last_match(1))
|
|
224
273
|
when /\Ae(!?)\s+(.+)\z/
|
|
225
274
|
vibe_edit_file(Regexp.last_match(2), force: Regexp.last_match(1) == "!")
|
|
275
|
+
when "run all"
|
|
276
|
+
run_vibe_all_markdown_code_blocks
|
|
226
277
|
when "run"
|
|
227
|
-
|
|
278
|
+
block = vibe_markdown_code_block_for_run(command_range)
|
|
279
|
+
if block
|
|
280
|
+
run_vibe_markdown_code_block(block)
|
|
281
|
+
else
|
|
282
|
+
vibe_record_undo { run_editor_buffer(source: vibe_command_source(command_range)) }
|
|
283
|
+
end
|
|
284
|
+
when /\Aprompt\s+(.+)\z/
|
|
285
|
+
action = editor_prompt_action(Regexp.last_match(1))
|
|
286
|
+
when "prompt"
|
|
287
|
+
@editor_state.status = "Prompt instruction required"
|
|
228
288
|
when "q"
|
|
229
289
|
vibe_quit_editor
|
|
230
290
|
when "q!"
|
|
@@ -237,16 +297,144 @@ module Kward
|
|
|
237
297
|
save_editor if @editor_state&.dirty?
|
|
238
298
|
close_editor if @editor_state
|
|
239
299
|
when /\A(?:(%|\d+,\d+))?s\/([^\/]*)\/([^\/]*)\/(g?)\z/
|
|
240
|
-
vibe_substitute_command(Regexp.last_match(1), Regexp.last_match(2), Regexp.last_match(3), global: Regexp.last_match(4) == "g")
|
|
300
|
+
vibe_substitute_command(command_range || Regexp.last_match(1), Regexp.last_match(2), Regexp.last_match(3), global: Regexp.last_match(4) == "g")
|
|
241
301
|
when /\A\d+\z/
|
|
242
302
|
@editor_state.set_cursor_line_and_column(command.to_i - 1, 0)
|
|
243
303
|
@editor_state.status = "Line #{command}"
|
|
244
304
|
else
|
|
245
305
|
@editor_state.status = "Unknown command: #{command}"
|
|
246
306
|
end
|
|
307
|
+
action || true
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def vibe_extract_command_range(command, visual_range)
|
|
311
|
+
if command.start_with?("'<,'>")
|
|
312
|
+
[visual_range, command[5..].to_s.strip]
|
|
313
|
+
elsif (match = command.match(/\A(%|\d+,\d+)(?=[a-z])/))
|
|
314
|
+
[match[1], command[match[1].length..].to_s]
|
|
315
|
+
else
|
|
316
|
+
[visual_range, command]
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def vibe_command_source(range)
|
|
321
|
+
return @editor_state.buffer.dup unless range
|
|
322
|
+
|
|
323
|
+
first_line, last_line = vibe_command_line_bounds(range)
|
|
324
|
+
start_index = @editor_state.line_range(first_line)[0]
|
|
325
|
+
end_index = @editor_state.line_range(last_line)[1]
|
|
326
|
+
@editor_state.buffer[start_index...end_index].to_s
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def vibe_command_line_bounds(range)
|
|
330
|
+
return range if range.is_a?(Array)
|
|
331
|
+
return [0, @editor_state.lines.length - 1] if range.to_s == "%"
|
|
332
|
+
|
|
333
|
+
first_line, last_line = range.to_s.split(",", 2).map { |value| value.to_i - 1 }
|
|
334
|
+
[first_line, last_line]
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def vibe_markdown_code_block_for_run(command_range)
|
|
338
|
+
if command_range.is_a?(Array)
|
|
339
|
+
MarkdownCodeBlock.for_selection(@editor_state.buffer, *command_range)
|
|
340
|
+
else
|
|
341
|
+
line, = @editor_state.cursor_line_and_column
|
|
342
|
+
MarkdownCodeBlock.for_cursor(@editor_state.buffer, line)
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def run_vibe_markdown_code_block(block)
|
|
347
|
+
document = block.source.dup
|
|
348
|
+
language = normalize_vibe_code_block_language(block.language)
|
|
349
|
+
unless language
|
|
350
|
+
@editor_state.status = "Selected code block has no runnable language"
|
|
351
|
+
return false
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
start_vibe_markdown_code_block_run(block, language) do |result, error|
|
|
355
|
+
if @editor_state.buffer != document
|
|
356
|
+
@editor_state.status = "Output not inserted: buffer changed"
|
|
357
|
+
next
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
output = error ? "Run failed: #{error.message}" : result.output
|
|
361
|
+
@editor_state.replace_buffer(block.with_output(output))
|
|
362
|
+
@editor_state.status = error ? "Output updated · Run failed" : vibe_code_block_result_status(result)
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def run_vibe_all_markdown_code_blocks
|
|
367
|
+
document = @editor_state.buffer.dup
|
|
368
|
+
runs = MarkdownCodeBlock.parse(document).filter_map do |block|
|
|
369
|
+
language = normalize_vibe_code_block_language(block.language)
|
|
370
|
+
[block, language] if language
|
|
371
|
+
end
|
|
372
|
+
if runs.empty?
|
|
373
|
+
@editor_state.status = "No runnable code blocks"
|
|
374
|
+
return false
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
results = []
|
|
378
|
+
run_next = nil
|
|
379
|
+
run_next = lambda do
|
|
380
|
+
block, language = runs.shift
|
|
381
|
+
start_vibe_markdown_code_block_run(block, language) do |result, error|
|
|
382
|
+
output = error ? "Run failed: #{error.message}" : result.output
|
|
383
|
+
results << [block, output, result, error]
|
|
384
|
+
if runs.empty?
|
|
385
|
+
finish_vibe_all_markdown_code_blocks(document, results)
|
|
386
|
+
else
|
|
387
|
+
run_next.call
|
|
388
|
+
end
|
|
389
|
+
end
|
|
390
|
+
end
|
|
391
|
+
run_next.call
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
def start_vibe_markdown_code_block_run(block, language, &completion)
|
|
395
|
+
run_editor_buffer(
|
|
396
|
+
source: block.code,
|
|
397
|
+
language: language,
|
|
398
|
+
source_path: ScratchpadLanguages.display_path(language),
|
|
399
|
+
output_visible: false,
|
|
400
|
+
completion: completion
|
|
401
|
+
)
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
def finish_vibe_all_markdown_code_blocks(document, results)
|
|
405
|
+
if @editor_state.buffer != document
|
|
406
|
+
@editor_state.status = "Output not inserted: buffer changed"
|
|
407
|
+
return false
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
content = document.dup
|
|
411
|
+
results.reverse_each do |block, output, _result, _error|
|
|
412
|
+
start_index, end_index, replacement = block.output_edit(output)
|
|
413
|
+
content[start_index...end_index] = replacement
|
|
414
|
+
end
|
|
415
|
+
@editor_state.replace_buffer(content)
|
|
416
|
+
failures = results.count { |_block, _output, result, error| error || result.exit_status != 0 }
|
|
417
|
+
status = "Ran #{results.length} code blocks"
|
|
418
|
+
status += " · #{failures} failed" if failures.positive?
|
|
419
|
+
@editor_state.status = status
|
|
247
420
|
true
|
|
248
421
|
end
|
|
249
422
|
|
|
423
|
+
def vibe_code_block_result_status(result)
|
|
424
|
+
return "Output updated · Canceled" if result.cancelled
|
|
425
|
+
|
|
426
|
+
"Output updated · Exit #{result.exit_status} · #{format_runner_duration(result.duration)}"
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
def normalize_vibe_code_block_language(language)
|
|
430
|
+
return nil if language.to_s.strip.empty?
|
|
431
|
+
|
|
432
|
+
normalized = ScratchpadLanguages.normalize(language)
|
|
433
|
+
ScratchpadLanguages.runnable?(normalized) ? normalized : nil
|
|
434
|
+
rescue ArgumentError
|
|
435
|
+
nil
|
|
436
|
+
end
|
|
437
|
+
|
|
250
438
|
def vibe_edit_file(path, force: false)
|
|
251
439
|
if @editor_state.dirty? && !force
|
|
252
440
|
@editor_state.status = "No write since last change (:e! overrides)"
|
|
@@ -258,6 +446,8 @@ module Kward
|
|
|
258
446
|
|
|
259
447
|
def vibe_complete_command_path
|
|
260
448
|
command = @editor_state.vibe_command.to_s
|
|
449
|
+
range_prefix = command.start_with?("'<,'>") ? command[0, 5] : ""
|
|
450
|
+
command = command[5..].to_s if range_prefix != ""
|
|
261
451
|
match = command.match(/\A(e!?)\s+(.*)\z/)
|
|
262
452
|
return false unless match
|
|
263
453
|
|
|
@@ -270,7 +460,7 @@ module Kward
|
|
|
270
460
|
|
|
271
461
|
replacement = candidates.length == 1 ? candidates.first : vibe_common_prefix(candidates)
|
|
272
462
|
if replacement.length > prefix.length
|
|
273
|
-
@editor_state.vibe_command = "#{match[1]} #{replacement}"
|
|
463
|
+
@editor_state.vibe_command = "#{range_prefix}#{match[1]} #{replacement}"
|
|
274
464
|
@editor_state.status = ":#{@editor_state.vibe_command}"
|
|
275
465
|
elsif candidates.length > 1
|
|
276
466
|
@editor_state.status = vibe_path_completion_status(candidates)
|
|
@@ -330,7 +520,9 @@ module Kward
|
|
|
330
520
|
|
|
331
521
|
start_line = 0
|
|
332
522
|
end_line = @editor_state.lines.length - 1
|
|
333
|
-
if range
|
|
523
|
+
if range.is_a?(Array)
|
|
524
|
+
start_line, end_line = range
|
|
525
|
+
elsif range&.include?(",")
|
|
334
526
|
start_line, end_line = range.split(",", 2).map { |value| value.to_i - 1 }
|
|
335
527
|
end
|
|
336
528
|
start_line = [[start_line, 0].max, @editor_state.lines.length - 1].min
|
|
@@ -400,8 +592,6 @@ module Kward
|
|
|
400
592
|
@editor_state.page_up(editor_page_rows)
|
|
401
593
|
when :ctrl_f
|
|
402
594
|
@editor_state.page_down(editor_page_rows)
|
|
403
|
-
when :ctrl_d
|
|
404
|
-
@editor_state.page_down(vibe_half_page_rows)
|
|
405
595
|
when :ctrl_u
|
|
406
596
|
@editor_state.page_up(vibe_half_page_rows)
|
|
407
597
|
when :ctrl_e
|
|
@@ -416,7 +606,7 @@ module Kward
|
|
|
416
606
|
end
|
|
417
607
|
|
|
418
608
|
def vibe_normal_control_key?(key)
|
|
419
|
-
["\n", "\r", "\b", "\x7F", TerminalKeys::CTRL_B, TerminalKeys::
|
|
609
|
+
["\n", "\r", "\b", "\x7F", TerminalKeys::CTRL_B, TerminalKeys::CTRL_E, TerminalKeys::CTRL_F, "\x0F", TerminalKeys::CTRL_R, TerminalKeys::CTRL_U, TerminalKeys::CTRL_Y].include?(key)
|
|
420
610
|
end
|
|
421
611
|
|
|
422
612
|
def vibe_visual_mode?
|
|
@@ -424,9 +614,9 @@ module Kward
|
|
|
424
614
|
end
|
|
425
615
|
|
|
426
616
|
def vibe_return_to_normal
|
|
427
|
-
|
|
617
|
+
@editor_state.collapse_to_primary_selection if @editor_state.multi_cursor?
|
|
428
618
|
@editor_state.vibe_mode = "normal"
|
|
429
|
-
@editor_state.status = "NORMAL · i insert · :w save · :q quit"
|
|
619
|
+
@editor_state.status = "NORMAL · i insert · :prompt ask agent · :w save · :q quit"
|
|
430
620
|
true
|
|
431
621
|
end
|
|
432
622
|
|
|
@@ -545,8 +735,6 @@ module Kward
|
|
|
545
735
|
@editor_state.page_down(editor_page_rows)
|
|
546
736
|
when TerminalKeys::CTRL_B
|
|
547
737
|
@editor_state.page_up(editor_page_rows)
|
|
548
|
-
when TerminalKeys::CTRL_D
|
|
549
|
-
@editor_state.page_down(vibe_half_page_rows)
|
|
550
738
|
when TerminalKeys::CTRL_U
|
|
551
739
|
@editor_state.page_up(vibe_half_page_rows)
|
|
552
740
|
when TerminalKeys::CTRL_E
|
|
@@ -647,6 +835,7 @@ module Kward
|
|
|
647
835
|
when "u"
|
|
648
836
|
@editor_state.undo
|
|
649
837
|
when ":"
|
|
838
|
+
@editor_state.vibe_command_range = nil
|
|
650
839
|
@editor_state.vibe_mode = "command"
|
|
651
840
|
@editor_state.vibe_command = ""
|
|
652
841
|
@editor_state.status = ":"
|
|
@@ -668,6 +857,13 @@ module Kward
|
|
|
668
857
|
end
|
|
669
858
|
|
|
670
859
|
def handle_vibe_visual_key(key)
|
|
860
|
+
if (direction = VIBE_VISUAL_LINE_MOVE_CONTROLS[key])
|
|
861
|
+
count, = vibe_count_and_body(@editor_state.vibe_pending.to_s + direction)
|
|
862
|
+
count = 1 if count.zero?
|
|
863
|
+
@editor_state.vibe_pending = ""
|
|
864
|
+
return vibe_move_selected_lines(direction, count)
|
|
865
|
+
end
|
|
866
|
+
|
|
671
867
|
key_name = key_name_for(key)
|
|
672
868
|
return handle_vibe_visual_named_key(key_name) if key_name
|
|
673
869
|
if key == "\e" || key == TerminalKeys::CTRL_C
|
|
@@ -702,9 +898,9 @@ module Kward
|
|
|
702
898
|
when "p"
|
|
703
899
|
vibe_paste_visual_selection
|
|
704
900
|
when "I"
|
|
705
|
-
|
|
901
|
+
vibe_begin_visual_multi_cursor_insert(:before)
|
|
706
902
|
when "A"
|
|
707
|
-
|
|
903
|
+
vibe_begin_visual_multi_cursor_insert(:after)
|
|
708
904
|
when ">"
|
|
709
905
|
vibe_indent_visual_selection(:right)
|
|
710
906
|
when "<"
|
|
@@ -729,6 +925,8 @@ module Kward
|
|
|
729
925
|
editor_search_repeat(vibe_opposite_search_direction)
|
|
730
926
|
when "o"
|
|
731
927
|
vibe_switch_visual_selection_end
|
|
928
|
+
when ":"
|
|
929
|
+
vibe_begin_visual_command
|
|
732
930
|
when "G"
|
|
733
931
|
vibe_visual_goto_line(command.match?(/\A\d+G\z/) ? count : nil)
|
|
734
932
|
when "gg"
|
|
@@ -779,6 +977,14 @@ module Kward
|
|
|
779
977
|
true
|
|
780
978
|
end
|
|
781
979
|
|
|
980
|
+
def vibe_begin_visual_command
|
|
981
|
+
@editor_state.vibe_command_range = vibe_visual_line_bounds
|
|
982
|
+
@editor_state.vibe_mode = "command"
|
|
983
|
+
@editor_state.vibe_command = "'<,'>"
|
|
984
|
+
@editor_state.status = ":'<,'>"
|
|
985
|
+
true
|
|
986
|
+
end
|
|
987
|
+
|
|
782
988
|
def vibe_jump_ruby_method(direction)
|
|
783
989
|
unless vibe_ruby_file?
|
|
784
990
|
@editor_state.status = "Ruby navigation requires Ruby file"
|
|
@@ -952,6 +1158,71 @@ module Kward
|
|
|
952
1158
|
vibe_apply_cursor_motion(motion, count)
|
|
953
1159
|
end
|
|
954
1160
|
|
|
1161
|
+
def vibe_move_selected_lines(direction, count)
|
|
1162
|
+
@editor_state.status = case @editor_state.vibe_mode
|
|
1163
|
+
when "visual_line" then "VISUAL LINE"
|
|
1164
|
+
when "visual_block" then "VISUAL BLOCK"
|
|
1165
|
+
else "VISUAL"
|
|
1166
|
+
end
|
|
1167
|
+
start_line, end_line = vibe_visual_line_bounds
|
|
1168
|
+
original_lines = @editor_state.lines.dup
|
|
1169
|
+
lines = original_lines.dup
|
|
1170
|
+
endpoint_positions = [@editor_state.selection_anchor, @editor_state.cursor].map do |offset|
|
|
1171
|
+
@editor_state.cursor_line_and_column_for(offset)
|
|
1172
|
+
end
|
|
1173
|
+
line_shift = 0
|
|
1174
|
+
|
|
1175
|
+
case direction
|
|
1176
|
+
when "h", "l"
|
|
1177
|
+
moved_lines = lines[start_line..end_line].map(&:dup)
|
|
1178
|
+
count.times do
|
|
1179
|
+
moved_lines = moved_lines.map do |line|
|
|
1180
|
+
if direction == "l"
|
|
1181
|
+
" #{line}"
|
|
1182
|
+
else
|
|
1183
|
+
line.sub(/\A(?: |\t| )/, "")
|
|
1184
|
+
end
|
|
1185
|
+
end
|
|
1186
|
+
end
|
|
1187
|
+
lines[start_line..end_line] = moved_lines
|
|
1188
|
+
when "k"
|
|
1189
|
+
line_shift = -[count, start_line].min
|
|
1190
|
+
return true if line_shift.zero?
|
|
1191
|
+
|
|
1192
|
+
selected_lines = lines.slice!(start_line..end_line)
|
|
1193
|
+
lines.insert(start_line + line_shift, *selected_lines)
|
|
1194
|
+
when "j"
|
|
1195
|
+
line_shift = [count, lines.length - end_line - 1].min
|
|
1196
|
+
return true if line_shift.zero?
|
|
1197
|
+
|
|
1198
|
+
selected_lines = lines.slice!(start_line..end_line)
|
|
1199
|
+
lines.insert(start_line + line_shift, *selected_lines)
|
|
1200
|
+
end
|
|
1201
|
+
|
|
1202
|
+
return false if lines == @editor_state.lines
|
|
1203
|
+
|
|
1204
|
+
@editor_state.replace_buffer(lines.join("\n"))
|
|
1205
|
+
selection_offsets = endpoint_positions.map do |line, column|
|
|
1206
|
+
new_line = line + line_shift
|
|
1207
|
+
new_column = if direction == "l"
|
|
1208
|
+
column + (lines[line].length - original_lines[line].length)
|
|
1209
|
+
elsif direction == "h"
|
|
1210
|
+
[column - (original_lines[line].length - lines[line].length), 0].max
|
|
1211
|
+
else
|
|
1212
|
+
column
|
|
1213
|
+
end
|
|
1214
|
+
@editor_state.offset_for_line_and_column(new_line, new_column)
|
|
1215
|
+
end
|
|
1216
|
+
@editor_state.set_selections([{ anchor: selection_offsets[0], cursor: selection_offsets[1] }])
|
|
1217
|
+
true
|
|
1218
|
+
end
|
|
1219
|
+
|
|
1220
|
+
def vibe_visual_line_bounds
|
|
1221
|
+
anchor_line, = @editor_state.cursor_line_and_column_for(@editor_state.selection_anchor)
|
|
1222
|
+
cursor_line, = @editor_state.cursor_line_and_column
|
|
1223
|
+
[anchor_line, cursor_line].minmax
|
|
1224
|
+
end
|
|
1225
|
+
|
|
955
1226
|
def vibe_visual_range
|
|
956
1227
|
@editor_state.selection_range
|
|
957
1228
|
end
|
|
@@ -1007,39 +1278,19 @@ module Kward
|
|
|
1007
1278
|
vibe_cancel_visual_mode
|
|
1008
1279
|
end
|
|
1009
1280
|
|
|
1010
|
-
def
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
@editor_state.
|
|
1019
|
-
|
|
1020
|
-
@editor_state.set_cursor_line_and_column(start_line, column)
|
|
1021
|
-
@editor_state.vibe_visual_block_insert[:start_index] = @editor_state.cursor
|
|
1022
|
-
@editor_state.vibe_mode = "insert"
|
|
1023
|
-
@editor_state.status = "INSERT · Esc normal"
|
|
1281
|
+
def vibe_begin_visual_multi_cursor_insert(position)
|
|
1282
|
+
vibe_remember_visual_selection
|
|
1283
|
+
start_line, end_line = vibe_visual_line_bounds
|
|
1284
|
+
selections = (start_line..end_line).map do |line_index|
|
|
1285
|
+
column = position == :before ? 0 : @editor_state.lines[line_index].to_s.length
|
|
1286
|
+
offset = @editor_state.offset_for_line_and_column(line_index, column)
|
|
1287
|
+
{ anchor: offset, cursor: offset }
|
|
1288
|
+
end
|
|
1289
|
+
@editor_state.set_selections(selections)
|
|
1290
|
+
vibe_enter_insert_mode(position == :before ? "I" : "A")
|
|
1024
1291
|
true
|
|
1025
1292
|
end
|
|
1026
1293
|
|
|
1027
|
-
def vibe_apply_visual_block_insert
|
|
1028
|
-
block = @editor_state.vibe_visual_block_insert
|
|
1029
|
-
@editor_state.vibe_visual_block_insert = nil
|
|
1030
|
-
return unless block
|
|
1031
|
-
|
|
1032
|
-
inserted_text = @editor_state.buffer[block[:start_index]...@editor_state.cursor].to_s
|
|
1033
|
-
return if inserted_text.empty?
|
|
1034
|
-
|
|
1035
|
-
block[:end_line].downto(block[:start_line] + 1) do |line_index|
|
|
1036
|
-
line_start = @editor_state.line_start_offset(line_index)
|
|
1037
|
-
line_length = @editor_state.lines[line_index].to_s.length
|
|
1038
|
-
@editor_state.cursor = line_start + [block[:column], line_length].min
|
|
1039
|
-
@editor_state.insert(inserted_text)
|
|
1040
|
-
end
|
|
1041
|
-
end
|
|
1042
|
-
|
|
1043
1294
|
def vibe_transform_visual_selection(transform)
|
|
1044
1295
|
range = vibe_visual_range
|
|
1045
1296
|
return false unless range
|
|
@@ -7,6 +7,8 @@ module Kward
|
|
|
7
7
|
private
|
|
8
8
|
|
|
9
9
|
def editor_layout(width, height = screen_height)
|
|
10
|
+
return editor_split_layout(width, height) if editor_runner_output_visible?
|
|
11
|
+
|
|
10
12
|
content_width = [width - 4, 1].max
|
|
11
13
|
visible_count = editor_visible_line_count(height: height, width: width)
|
|
12
14
|
line_index, column = @editor_state.cursor_line_and_column
|
|
@@ -21,7 +23,107 @@ module Kward
|
|
|
21
23
|
end
|
|
22
24
|
end
|
|
23
25
|
|
|
24
|
-
def
|
|
26
|
+
def editor_split_layout(width, height)
|
|
27
|
+
content_width = [width - 4, 1].max
|
|
28
|
+
source_count, output_count = editor_split_visible_counts(height: height, width: width)
|
|
29
|
+
line_index, column = @editor_state.cursor_line_and_column
|
|
30
|
+
gutter_width = editor_line_number_gutter_width
|
|
31
|
+
text_width = editor_text_width(content_width, gutter_width)
|
|
32
|
+
sync_editor_wrap_state(text_width)
|
|
33
|
+
source_rows, cursor_row, cursor_col = if current_editor_soft_wrap?
|
|
34
|
+
editor_wrapped_layout(width, content_width, source_count, line_index, column, text_width, include_bottom: false)
|
|
35
|
+
else
|
|
36
|
+
editor_unwrapped_layout(width, content_width, source_count, line_index, column, text_width, include_bottom: false)
|
|
37
|
+
end
|
|
38
|
+
rows = source_rows + editor_runner_output_rows(width, content_width, output_count)
|
|
39
|
+
rows.concat(editor_bottom_rows(width))
|
|
40
|
+
[rows, cursor_row, cursor_col]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def editor_split_visible_counts(height:, width:)
|
|
44
|
+
# Keep one row available above the composer, matching the normal editor layout.
|
|
45
|
+
available = height - editor_bottom_rows(width).length - 5
|
|
46
|
+
available = 2 if available < 2
|
|
47
|
+
source_count = (available + 1) / 2
|
|
48
|
+
[source_count, available - source_count]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def editor_runner_output_rows(width, content_width, visible_count, include_bottom: false)
|
|
52
|
+
lines = editor_runner_output_lines(content_width)
|
|
53
|
+
maximum_scroll = [lines.length - visible_count, 0].max
|
|
54
|
+
@editor_runner_state.set_scroll_row(@editor_runner_state.scroll_row, maximum: maximum_scroll)
|
|
55
|
+
visible_lines = lines[@editor_runner_state.scroll_row, visible_count] || []
|
|
56
|
+
visible_lines << "" while visible_lines.length < visible_count
|
|
57
|
+
rows = [editor_runner_output_top_border(width)]
|
|
58
|
+
rows.concat(visible_lines.each_with_index.map do |line, index|
|
|
59
|
+
line_index = @editor_runner_state.scroll_row + index
|
|
60
|
+
selection = editor_runner_selection_range_for(line_index, ANSI.strip(line).length)
|
|
61
|
+
line = editor_overlay_selection(line, [selection], force: true) if selection
|
|
62
|
+
box_content_row(line, content_width)
|
|
63
|
+
end)
|
|
64
|
+
rows << footer_row(content_width, editor_runner_output_status)
|
|
65
|
+
rows.concat(editor_bottom_rows(width)) if include_bottom
|
|
66
|
+
rows
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def editor_runner_output_visible_count(height: screen_height, width: screen_width)
|
|
70
|
+
editor_split_visible_counts(height: height, width: width).last
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def editor_runner_output_scroll_max
|
|
74
|
+
width = [screen_width - 4, 1].max
|
|
75
|
+
lines = editor_runner_output_lines(width)
|
|
76
|
+
[lines.length - editor_runner_output_visible_count, 0].max
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def editor_runner_output_lines(width)
|
|
80
|
+
text = ANSI.sanitize_transcript(@editor_runner_state.output_text)
|
|
81
|
+
text = ANSI.strip(text) unless @color_enabled
|
|
82
|
+
text = text.chomp
|
|
83
|
+
return ["(no output)"] if text.empty?
|
|
84
|
+
|
|
85
|
+
text.split("\n", -1).flat_map do |line|
|
|
86
|
+
wrapped = ANSI.wrap_visible(line, width)
|
|
87
|
+
wrapped.empty? ? [""] : wrapped
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def editor_runner_output_top_border(width)
|
|
92
|
+
title = visible_truncate("Output #{editor_display_path}", [width - 4, 1].max)
|
|
93
|
+
plain_title = ANSI.strip(title)
|
|
94
|
+
"#{colored("╭", :primary_green)} #{title} #{colored("─" * [width - plain_title.length - 4, 0].max, :primary_green)}#{colored("╮", :primary_green)}"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def editor_runner_output_status
|
|
98
|
+
state = @editor_runner_state
|
|
99
|
+
if state.running?
|
|
100
|
+
return "Canceling #{state.language} · please wait" if state.cancel_requested?
|
|
101
|
+
|
|
102
|
+
return "Running #{state.language} · Ctrl+C cancel · Esc close"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
if (error = state.error)
|
|
106
|
+
return "Run failed: #{error.message} · Esc close"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
result = state.result
|
|
110
|
+
status = if result.cancelled
|
|
111
|
+
"Canceled"
|
|
112
|
+
else
|
|
113
|
+
"Exit #{result.exit_status}"
|
|
114
|
+
end
|
|
115
|
+
status += " · #{format_runner_duration(result.duration)}"
|
|
116
|
+
status += " · output truncated" if result.truncated
|
|
117
|
+
status += " · source changed" if editor_runner_output_stale?
|
|
118
|
+
"#{status} · Ctrl+R rerun · Esc close"
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def editor_runner_output_stale?
|
|
122
|
+
digest = Digest::SHA256.hexdigest(@editor_state.buffer.to_s)
|
|
123
|
+
digest != @editor_runner_state.source_digest
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def editor_unwrapped_layout(width, content_width, visible_count, line_index, column, text_width, include_bottom: true)
|
|
25
127
|
@editor_state.viewport_row = [[@editor_state.viewport_row, line_index - visible_count + 1].max, line_index].min
|
|
26
128
|
@editor_state.viewport_row = [@editor_state.viewport_row, 0].max
|
|
27
129
|
@editor_state.viewport_column = [[@editor_state.viewport_column.to_i, column - text_width + 1].max, column].min
|
|
@@ -43,13 +145,13 @@ module Kward
|
|
|
43
145
|
box_content_row(row, content_width)
|
|
44
146
|
end)
|
|
45
147
|
rows << footer_row(content_width, editor_status_text)
|
|
46
|
-
rows.concat(editor_bottom_rows(width))
|
|
148
|
+
rows.concat(editor_bottom_rows(width)) if include_bottom
|
|
47
149
|
cursor_row = 1 + line_index - @editor_state.viewport_row
|
|
48
150
|
cursor_col = 2 + gutter_width + [[column - @editor_state.viewport_column, 0].max, text_width - 1].min
|
|
49
151
|
[rows, cursor_row, cursor_col]
|
|
50
152
|
end
|
|
51
153
|
|
|
52
|
-
def editor_wrapped_layout(width, content_width, visible_count, line_index, column, text_width)
|
|
154
|
+
def editor_wrapped_layout(width, content_width, visible_count, line_index, column, text_width, include_bottom: true)
|
|
53
155
|
visual_rows = editor_visual_rows(text_width)
|
|
54
156
|
cursor_visual_row = editor_visual_row_for(line_index, column, text_width)
|
|
55
157
|
@editor_state.viewport_row = [[@editor_state.viewport_row, cursor_visual_row - visible_count + 1].max, cursor_visual_row].min
|
|
@@ -67,7 +169,7 @@ module Kward
|
|
|
67
169
|
end
|
|
68
170
|
end)
|
|
69
171
|
rows << footer_row(content_width, editor_status_text)
|
|
70
|
-
rows.concat(editor_bottom_rows(width))
|
|
172
|
+
rows.concat(editor_bottom_rows(width)) if include_bottom
|
|
71
173
|
line_start = editor_visual_row_start_column(line_index, column, text_width)
|
|
72
174
|
cursor_row = 1 + cursor_visual_row - @editor_state.viewport_row
|
|
73
175
|
cursor_col = 2 + editor_line_number_gutter_width + [[column - line_start, 0].max, text_width - 1].min
|
|
@@ -138,8 +240,8 @@ module Kward
|
|
|
138
240
|
editor_overlay_selection(rendered, cursor_columns.map { |column| [column, column + 1] })
|
|
139
241
|
end
|
|
140
242
|
|
|
141
|
-
def editor_overlay_selection(rendered, selection_ranges)
|
|
142
|
-
return rendered unless @color_enabled
|
|
243
|
+
def editor_overlay_selection(rendered, selection_ranges, force: false)
|
|
244
|
+
return rendered unless @color_enabled || force
|
|
143
245
|
|
|
144
246
|
output = +""
|
|
145
247
|
selected = false
|
|
@@ -264,6 +366,7 @@ module Kward
|
|
|
264
366
|
|
|
265
367
|
def editor_status_text
|
|
266
368
|
text = @editor_state.search_active ? "#{@editor_state.search_direction == :backward ? "Search backward" : "Search"}: #{@editor_state.search_query}" : @editor_state.status
|
|
369
|
+
text = "#{text} · #{spinner_frame}" if @editor_agent_suspended && @busy
|
|
267
370
|
visible_truncate(text, [screen_width - 4, 1].max)
|
|
268
371
|
end
|
|
269
372
|
end
|