kward 0.83.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 +68 -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/api.md +4 -0
- data/doc/composer.md +1 -1
- data/doc/configuration.md +68 -21
- data/doc/editor.md +28 -13
- data/doc/files.md +8 -4
- data/doc/getting-started.md +3 -0
- data/doc/pan.md +19 -15
- data/doc/platform-support.md +48 -0
- data/doc/security.md +3 -2
- data/doc/shell.md +62 -45
- data/doc/troubleshooting.md +12 -2
- data/doc/usage.md +5 -5
- data/kward.gemspec +5 -4
- data/lib/kward/agent.rb +6 -3
- data/lib/kward/ansi.rb +110 -10
- 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 +48 -7
- 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 +118 -55
- data/lib/kward/cli/slash_commands.rb +12 -14
- data/lib/kward/cli/tabs.rb +83 -12
- data/lib/kward/cli/tool_summaries.rb +14 -0
- data/lib/kward/cli.rb +45 -7
- data/lib/kward/cli_transcript_formatter.rb +11 -4
- data/lib/kward/config_files.rb +82 -68
- data/lib/kward/detached_run.rb +44 -0
- data/lib/kward/interactive_pty_runner.rb +102 -28
- data/lib/kward/{ekwsh.rb → kwsh.rb} +35 -16
- data/lib/kward/kwshrc.rb +233 -0
- data/lib/kward/markdown_code_block.rb +136 -0
- data/lib/kward/model/client.rb +34 -22
- 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 +119 -26
- data/lib/kward/project_files.rb +2 -2
- data/lib/kward/prompt_interface/composer_renderer.rb +44 -40
- data/lib/kward/prompt_interface/composer_state.rb +33 -24
- data/lib/kward/prompt_interface/editor/auto_indent.rb +24 -22
- data/lib/kward/prompt_interface/editor/controller.rb +30 -33
- data/lib/kward/prompt_interface/editor/endwise.rb +13 -4
- data/lib/kward/prompt_interface/editor/modes/vibe.rb +289 -44
- data/lib/kward/prompt_interface/editor/renderer.rb +108 -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 +10 -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/file_overlay.rb +71 -15
- data/lib/kward/prompt_interface/key_handler.rb +67 -0
- data/lib/kward/prompt_interface/overlay_renderer.rb +7 -5
- data/lib/kward/prompt_interface/project_browser.rb +415 -14
- data/lib/kward/prompt_interface/runtime_state.rb +50 -1
- 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 +93 -18
- data/lib/kward/prompts/commands.rb +1 -1
- data/lib/kward/prompts.rb +1 -1
- data/lib/kward/pty_output_sink.rb +47 -0
- 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 +2 -0
- data/lib/kward/terminal_keys.rb +12 -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/registry.rb +13 -4
- 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 +36 -8
- data/templates/default/fulldoc/html/images/kward_screen_1.png +0 -0
data/lib/kward/project_files.rb
CHANGED
|
@@ -15,13 +15,13 @@ module Kward
|
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
def git_paths(root, include_ignored: false)
|
|
18
|
-
output, status = Open3.
|
|
18
|
+
output, status = Open3.capture2e("git", "ls-files", "--cached", "--others", "--exclude-standard", chdir: root)
|
|
19
19
|
return [] unless status.success?
|
|
20
20
|
|
|
21
21
|
paths = output.lines.map(&:chomp).reject(&:empty?)
|
|
22
22
|
return paths unless include_ignored
|
|
23
23
|
|
|
24
|
-
ignored_output, ignored_status = Open3.
|
|
24
|
+
ignored_output, ignored_status = Open3.capture2e("git", "ls-files", "--others", "--ignored", "--exclude-standard", chdir: root)
|
|
25
25
|
return paths unless ignored_status.success?
|
|
26
26
|
|
|
27
27
|
paths + ignored_output.lines.map(&:chomp).reject(&:empty?)
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
require_relative "../terminal_text"
|
|
2
|
+
|
|
1
3
|
# Namespace for the Kward CLI agent runtime.
|
|
2
4
|
module Kward
|
|
3
5
|
# Renderer for the editable composer text area.
|
|
@@ -35,45 +37,42 @@ module Kward
|
|
|
35
37
|
end
|
|
36
38
|
|
|
37
39
|
def compact_composer_layout(width)
|
|
38
|
-
cursor_line, cursor_col =
|
|
40
|
+
lines, cursor_line, cursor_col = @composer.lines_and_cursor
|
|
39
41
|
prefix = "#{@prompt_label} "
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
prefix_width = TerminalText.width(prefix)
|
|
43
|
+
line = lines[cursor_line] || ""
|
|
44
|
+
input_width = [width - prefix_width, 1].max
|
|
45
|
+
layout = TerminalText.wrap(line, width: input_width, cursor: cursor_col)
|
|
46
|
+
visible = layout[:rows][layout[:cursor_row]].to_s
|
|
47
|
+
row = visible_ljust(TerminalText.truncate("#{prefix}#{visible}", width), width)
|
|
48
|
+
[[row], 0, [prefix_width + layout[:cursor_col], width - 1].min]
|
|
46
49
|
end
|
|
47
50
|
|
|
48
51
|
def input_layout(content_width)
|
|
49
|
-
cursor_line, cursor_col =
|
|
52
|
+
lines, cursor_line, cursor_col = @composer.lines_and_cursor
|
|
50
53
|
rows = []
|
|
51
54
|
cursor_row = 0
|
|
55
|
+
cursor_col_in_row = 0
|
|
52
56
|
rendered_row_offset = 0
|
|
53
57
|
|
|
54
|
-
|
|
58
|
+
lines.each_with_index do |line, index|
|
|
55
59
|
prefix = input_prefix(index)
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if index == cursor_line && cursor_col == line.length && line.length.positive? && (line.length % available).zero?
|
|
61
|
-
chunks << ""
|
|
62
|
-
end
|
|
60
|
+
prefix_width = TerminalText.width(prefix)
|
|
61
|
+
continuation_prefix = " " * prefix_width
|
|
62
|
+
available = [content_width - prefix_width, 1].max
|
|
63
|
+
layout = TerminalText.wrap(line, width: available, cursor: index == cursor_line ? cursor_col : nil)
|
|
63
64
|
|
|
64
65
|
if index == cursor_line
|
|
65
|
-
cursor_row = rendered_row_offset +
|
|
66
|
+
cursor_row = rendered_row_offset + layout[:cursor_row]
|
|
67
|
+
cursor_col_in_row = prefix_width + layout[:cursor_col]
|
|
66
68
|
end
|
|
67
69
|
|
|
68
|
-
|
|
70
|
+
layout[:rows].each_with_index do |chunk, chunk_index|
|
|
69
71
|
rows << "#{chunk_index.zero? ? prefix : continuation_prefix}#{chunk}"
|
|
70
72
|
end
|
|
71
|
-
rendered_row_offset +=
|
|
73
|
+
rendered_row_offset += layout[:rows].length
|
|
72
74
|
end
|
|
73
75
|
|
|
74
|
-
prefix = input_prefix(cursor_line)
|
|
75
|
-
available = [content_width - prefix.length, 1].max
|
|
76
|
-
cursor_col_in_row = prefix.length + (cursor_col % available)
|
|
77
76
|
[rows, cursor_row, cursor_col_in_row]
|
|
78
77
|
end
|
|
79
78
|
|
|
@@ -81,23 +80,28 @@ module Kward
|
|
|
81
80
|
title = composer_title
|
|
82
81
|
status = cached_composer_status_text
|
|
83
82
|
if status
|
|
84
|
-
gap = width - 2 - ANSI.strip(title)
|
|
83
|
+
gap = width - 2 - TerminalText.width(ANSI.strip(title)) - TerminalText.width(ANSI.strip(status))
|
|
85
84
|
if gap >= 0
|
|
86
85
|
return colored("╭", :primary_green) + title + colored("─" * gap, :primary_green) + status + colored("╮", :primary_green)
|
|
87
86
|
end
|
|
88
87
|
end
|
|
89
88
|
plain_title = ANSI.strip(title)
|
|
90
|
-
"#{colored("╭", :primary_green)}#{title}#{colored("─" * [width - plain_title
|
|
89
|
+
"#{colored("╭", :primary_green)}#{title}#{colored("─" * [width - TerminalText.width(plain_title) - 2, 0].max, :primary_green)}#{colored("╮", :primary_green)}"
|
|
91
90
|
end
|
|
92
91
|
|
|
93
92
|
def composer_title
|
|
94
93
|
label = composer_title_label
|
|
95
94
|
if @busy && @queued_count.positive?
|
|
96
|
-
status_composer_text(busy_title("#{label} · #{@queued_count} queued"))
|
|
95
|
+
status_composer_text(busy_title("#{label} · #{colored("#{@queued_count} queued", :metadata)}"))
|
|
97
96
|
elsif @busy && @steered_count.to_i.positive?
|
|
98
|
-
status_composer_text(busy_title("#{label} · #{spinner_frame} steering"))
|
|
97
|
+
status_composer_text(busy_title("#{label} · #{colored(spinner_frame, :activity, :bold)} steering"))
|
|
98
|
+
elsif @busy && response_arriving?
|
|
99
|
+
status_composer_text(busy_title("#{label} · #{colored("✦", :activity, :bold)} responding"))
|
|
99
100
|
elsif @busy
|
|
100
|
-
status_composer_text(busy_title("#{label} · #{spinner_frame} #{@busy_activity}"))
|
|
101
|
+
status_composer_text(busy_title("#{label} · #{colored(spinner_frame, :activity, :bold)} #{@busy_activity}"))
|
|
102
|
+
elsif @completion_status
|
|
103
|
+
completion = colored(@completion_status[:text], @completion_status[:style], :bold)
|
|
104
|
+
status_composer_text("#{label} · #{completion}")
|
|
101
105
|
else
|
|
102
106
|
status_composer_text(label)
|
|
103
107
|
end
|
|
@@ -110,6 +114,8 @@ module Kward
|
|
|
110
114
|
end
|
|
111
115
|
|
|
112
116
|
def busy_title(text)
|
|
117
|
+
elapsed = busy_elapsed_text
|
|
118
|
+
text = "#{text} · #{colored(elapsed, :metadata)}" if elapsed
|
|
113
119
|
@busy_help ? "#{text} · #{BUSY_HELP_TEXT}" : text
|
|
114
120
|
end
|
|
115
121
|
|
|
@@ -140,7 +146,7 @@ module Kward
|
|
|
140
146
|
label_left = 4
|
|
141
147
|
@tabs.each_with_index.map do |label, index|
|
|
142
148
|
text = tab_label(label, index)
|
|
143
|
-
width = ANSI.strip(text)
|
|
149
|
+
width = TerminalText.width(ANSI.strip(text))
|
|
144
150
|
slot = {
|
|
145
151
|
left: label_left - 2,
|
|
146
152
|
label_left: label_left,
|
|
@@ -185,18 +191,25 @@ module Kward
|
|
|
185
191
|
|
|
186
192
|
visible_offset = 0
|
|
187
193
|
last_index = nil
|
|
188
|
-
text.to_s.scan(/\e\[[0-9;:]*m
|
|
194
|
+
text.to_s.scan(/\e\[[0-9;:]*m|\X/m).each do |part|
|
|
189
195
|
if part.start_with?("\e")
|
|
190
196
|
index = visible_offset.positive? ? last_index : left
|
|
191
197
|
row[index] = row[index].to_s + part if index&.between?(0, row.length - 1)
|
|
192
198
|
next
|
|
193
199
|
end
|
|
194
200
|
|
|
201
|
+
part_width = TerminalText.width(part)
|
|
202
|
+
if part_width.zero?
|
|
203
|
+
row[last_index] = row[last_index].to_s + part if last_index
|
|
204
|
+
next
|
|
205
|
+
end
|
|
206
|
+
|
|
195
207
|
index = left + visible_offset
|
|
196
208
|
break if index >= row.length
|
|
197
209
|
row[index] = row[index].to_s.sub(/\A /, "") + part unless index.negative?
|
|
210
|
+
1.upto(part_width - 1) { |offset| row[index + offset] = "" if (index + offset).between?(0, row.length - 1) }
|
|
198
211
|
last_index = index
|
|
199
|
-
visible_offset +=
|
|
212
|
+
visible_offset += part_width
|
|
200
213
|
end
|
|
201
214
|
end
|
|
202
215
|
|
|
@@ -252,19 +265,10 @@ module Kward
|
|
|
252
265
|
[[input_cap, height - 3 - overlay_count - footer_count - attachment_count].min, 1].max
|
|
253
266
|
end
|
|
254
267
|
|
|
255
|
-
def input_lines
|
|
256
|
-
lines = composer_input.split("\n", -1)
|
|
257
|
-
lines.empty? ? [""] : lines
|
|
258
|
-
end
|
|
259
|
-
|
|
260
268
|
def input_prefix(_index)
|
|
261
269
|
""
|
|
262
270
|
end
|
|
263
271
|
|
|
264
|
-
def cursor_logical_position
|
|
265
|
-
@composer.cursor_logical_position
|
|
266
|
-
end
|
|
267
|
-
|
|
268
272
|
def image_viewer_preview_rows(height, width: screen_width)
|
|
269
273
|
available_rows = max_project_browser_rows(height, width: width)
|
|
270
274
|
desired_rows = [(IMAGE_VIEWER_MAX_ROWS * image_viewer_zoom).round, 1].max
|
|
@@ -276,7 +280,7 @@ module Kward
|
|
|
276
280
|
preview_rows = image_viewer_preview_rows(height, width: width)
|
|
277
281
|
title = visible_truncate(" Image: #{@image_viewer_state[:display_path]} ", content_width)
|
|
278
282
|
rows = [
|
|
279
|
-
colored("╭", :primary_green) + title + colored("─" * [width - ANSI.strip(title)
|
|
283
|
+
colored("╭", :primary_green) + title + colored("─" * [width - TerminalText.width(ANSI.strip(title)) - 2, 0].max, :primary_green) + colored("╮", :primary_green),
|
|
280
284
|
image_viewer_graphic_row(width, image_viewer_sequence(width, height))
|
|
281
285
|
]
|
|
282
286
|
rows.concat(Array.new(preview_rows - 1) { box_content_row("", content_width) })
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
require_relative "../text_boundary"
|
|
2
|
+
require_relative "../text_matcher"
|
|
3
|
+
require_relative "../terminal_text"
|
|
2
4
|
|
|
3
5
|
# Namespace for the Kward CLI agent runtime.
|
|
4
6
|
module Kward
|
|
@@ -41,6 +43,8 @@ module Kward
|
|
|
41
43
|
@history_search_query = nil
|
|
42
44
|
@history_search_draft = nil
|
|
43
45
|
@history_search_index = 0
|
|
46
|
+
@history_search_matches_query = nil
|
|
47
|
+
@history_search_matches = nil
|
|
44
48
|
end
|
|
45
49
|
|
|
46
50
|
# Removes all pending attachments without changing text input.
|
|
@@ -76,31 +80,33 @@ module Kward
|
|
|
76
80
|
@cursor += string.length
|
|
77
81
|
end
|
|
78
82
|
|
|
79
|
-
# Deletes one
|
|
83
|
+
# Deletes one grapheme before the cursor.
|
|
80
84
|
def delete_before_cursor
|
|
81
85
|
return false if @cursor.zero?
|
|
82
86
|
|
|
83
|
-
|
|
84
|
-
@
|
|
87
|
+
previous = TerminalText.previous_grapheme_boundary(@input, @cursor)
|
|
88
|
+
@input = @input[0...previous] + @input[@cursor..]
|
|
89
|
+
@cursor = previous
|
|
85
90
|
true
|
|
86
91
|
end
|
|
87
92
|
|
|
88
|
-
# Deletes one
|
|
93
|
+
# Deletes one grapheme at the cursor without moving it.
|
|
89
94
|
def delete_at_cursor
|
|
90
95
|
return false unless @cursor < @input.length
|
|
91
96
|
|
|
92
|
-
|
|
97
|
+
following = TerminalText.next_grapheme_boundary(@input, @cursor)
|
|
98
|
+
@input = @input[0...@cursor] + @input[following..]
|
|
93
99
|
true
|
|
94
100
|
end
|
|
95
101
|
|
|
96
|
-
# Moves the cursor one
|
|
102
|
+
# Moves the cursor one grapheme left when possible.
|
|
97
103
|
def move_cursor_left
|
|
98
|
-
@cursor
|
|
104
|
+
@cursor = TerminalText.previous_grapheme_boundary(@input, @cursor) if @cursor.positive?
|
|
99
105
|
end
|
|
100
106
|
|
|
101
|
-
# Moves the cursor one
|
|
107
|
+
# Moves the cursor one grapheme right when possible.
|
|
102
108
|
def move_cursor_right
|
|
103
|
-
@cursor
|
|
109
|
+
@cursor = TerminalText.next_grapheme_boundary(@input, @cursor) if @cursor < @input.length
|
|
104
110
|
end
|
|
105
111
|
|
|
106
112
|
# Moves the cursor to the beginning of the input buffer.
|
|
@@ -164,10 +170,14 @@ module Kward
|
|
|
164
170
|
@cursor = @input.length
|
|
165
171
|
end
|
|
166
172
|
|
|
167
|
-
# Returns `[row, column]` for cursor
|
|
168
|
-
def
|
|
173
|
+
# Returns `[lines, row, column]` for cursor-aware multi-line layout.
|
|
174
|
+
def lines_and_cursor
|
|
175
|
+
lines = @input.split("\n", -1)
|
|
169
176
|
before_cursor = @input[0...@cursor]
|
|
170
|
-
|
|
177
|
+
row = before_cursor.count("\n")
|
|
178
|
+
line_start = before_cursor.rindex("\n")
|
|
179
|
+
column = line_start ? before_cursor.length - line_start - 1 : before_cursor.length
|
|
180
|
+
[lines.empty? ? [""] : lines, row, column]
|
|
171
181
|
end
|
|
172
182
|
|
|
173
183
|
# Replaces the in-memory history list with persisted entries.
|
|
@@ -175,6 +185,7 @@ module Kward
|
|
|
175
185
|
@history = Array(values).map(&:to_s).reject { |value| value.strip.empty? }
|
|
176
186
|
reset_history_navigation
|
|
177
187
|
reset_history_search
|
|
188
|
+
invalidate_history_search_matches
|
|
178
189
|
end
|
|
179
190
|
|
|
180
191
|
# Stores a submitted input unless it is blank or duplicates the previous entry.
|
|
@@ -184,6 +195,7 @@ module Kward
|
|
|
184
195
|
return false if @history.last == value
|
|
185
196
|
|
|
186
197
|
@history << value
|
|
198
|
+
invalidate_history_search_matches
|
|
187
199
|
true
|
|
188
200
|
end
|
|
189
201
|
|
|
@@ -234,9 +246,13 @@ module Kward
|
|
|
234
246
|
|
|
235
247
|
def history_search_matches
|
|
236
248
|
query = @history_search_query.to_s.downcase
|
|
237
|
-
return @
|
|
249
|
+
return @history_search_matches if @history_search_matches_query == query && @history_search_matches
|
|
238
250
|
|
|
239
|
-
|
|
251
|
+
pattern = TextMatcher.subsequence_pattern(query)
|
|
252
|
+
@history_search_matches_query = query
|
|
253
|
+
@history_search_matches = @history.reverse.select do |value|
|
|
254
|
+
TextMatcher.subsequence?(value.downcase, query, pattern)
|
|
255
|
+
end
|
|
240
256
|
end
|
|
241
257
|
|
|
242
258
|
def selected_history_search_match
|
|
@@ -274,16 +290,9 @@ module Kward
|
|
|
274
290
|
@history_search_index = 0
|
|
275
291
|
end
|
|
276
292
|
|
|
277
|
-
def
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
if index
|
|
281
|
-
value = value[(index + 1)..].to_s
|
|
282
|
-
true
|
|
283
|
-
else
|
|
284
|
-
false
|
|
285
|
-
end
|
|
286
|
-
end
|
|
293
|
+
def invalidate_history_search_matches
|
|
294
|
+
@history_search_matches_query = nil
|
|
295
|
+
@history_search_matches = nil
|
|
287
296
|
end
|
|
288
297
|
end
|
|
289
298
|
end
|
|
@@ -145,7 +145,7 @@ module Kward
|
|
|
145
145
|
line = @editor_state.lines[line_index].to_s
|
|
146
146
|
before_cursor = line[0...column].to_s
|
|
147
147
|
base_indent = line[/\A[ \t]*/].to_s
|
|
148
|
-
language =
|
|
148
|
+
language = editor_effective_syntax_language(line_index)
|
|
149
149
|
indent = base_indent.dup
|
|
150
150
|
indent += editor_indent_unit if editor_line_opens_indent?(before_cursor, language)
|
|
151
151
|
indent
|
|
@@ -179,7 +179,8 @@ module Kward
|
|
|
179
179
|
|
|
180
180
|
def editor_expected_indent_for_line(line_index)
|
|
181
181
|
line = @editor_state.lines[line_index].to_s
|
|
182
|
-
|
|
182
|
+
language = editor_effective_syntax_language(line_index)
|
|
183
|
+
code = editor_indent_code(line, language).strip
|
|
183
184
|
matching_indent = editor_matching_indent_for_line(code, line_index)
|
|
184
185
|
return matching_indent if matching_indent
|
|
185
186
|
|
|
@@ -187,7 +188,7 @@ module Kward
|
|
|
187
188
|
return "" unless previous_line
|
|
188
189
|
|
|
189
190
|
indent = previous_line[:indent].dup
|
|
190
|
-
indent += editor_indent_unit if editor_line_opens_indent?(previous_line[:code],
|
|
191
|
+
indent += editor_indent_unit if editor_line_opens_indent?(previous_line[:code], previous_line[:language])
|
|
191
192
|
indent
|
|
192
193
|
end
|
|
193
194
|
|
|
@@ -195,18 +196,12 @@ module Kward
|
|
|
195
196
|
return nil if code.empty?
|
|
196
197
|
return editor_matching_punctuation_indent(code[0], line_index) if editor_closing_punctuation?(code[0])
|
|
197
198
|
|
|
198
|
-
|
|
199
|
+
language = editor_effective_syntax_language(line_index)
|
|
200
|
+
editor_matching_word_indent(line_index) if editor_completed_word_closer?(code, language)
|
|
199
201
|
end
|
|
200
202
|
|
|
201
203
|
def previous_non_blank_editor_line(line_index)
|
|
202
|
-
(line_index
|
|
203
|
-
line = @editor_state.lines[index].to_s
|
|
204
|
-
code = editor_indent_code(line, editor_syntax_language).rstrip
|
|
205
|
-
next if code.strip.empty?
|
|
206
|
-
|
|
207
|
-
return { indent: line[/\A[ \t]*/].to_s, code: code }
|
|
208
|
-
end
|
|
209
|
-
nil
|
|
204
|
+
editor_previous_code_lines(line_index).last
|
|
210
205
|
end
|
|
211
206
|
|
|
212
207
|
def editor_update_current_line_indent(indent, preserve_content_column: false)
|
|
@@ -248,7 +243,7 @@ module Kward
|
|
|
248
243
|
line = @editor_state.lines[line_index].to_s
|
|
249
244
|
before_cursor = line[0...column].to_s
|
|
250
245
|
base_indent = line[/\A[ \t]*/].to_s
|
|
251
|
-
language =
|
|
246
|
+
language = editor_effective_syntax_language(line_index)
|
|
252
247
|
opens_indent = editor_line_opens_indent?(before_cursor, language)
|
|
253
248
|
paired_closer = editor_next_paired_closer
|
|
254
249
|
return nil unless paired_closer
|
|
@@ -410,7 +405,8 @@ module Kward
|
|
|
410
405
|
end
|
|
411
406
|
|
|
412
407
|
def editor_closing_punctuation?(text)
|
|
413
|
-
|
|
408
|
+
line_index = @editor_state.cursor_line_and_column.first
|
|
409
|
+
PUNCTUATION_PAIRS.key?(text) && PUNCTUATION_INDENT_LANGUAGES.include?(editor_effective_syntax_language(line_index))
|
|
414
410
|
end
|
|
415
411
|
|
|
416
412
|
def editor_reindent_for_closing_punctuation(text)
|
|
@@ -425,7 +421,7 @@ module Kward
|
|
|
425
421
|
line_index, column = @editor_state.cursor_line_and_column
|
|
426
422
|
line = @editor_state.lines[line_index].to_s
|
|
427
423
|
before_cursor = line[0...column].to_s
|
|
428
|
-
return unless editor_completed_word_closer?(before_cursor,
|
|
424
|
+
return unless editor_completed_word_closer?(before_cursor, editor_effective_syntax_language(line_index))
|
|
429
425
|
|
|
430
426
|
indent = editor_matching_word_indent(line_index)
|
|
431
427
|
editor_reindent_current_line(indent) if indent
|
|
@@ -477,7 +473,8 @@ module Kward
|
|
|
477
473
|
end
|
|
478
474
|
|
|
479
475
|
def editor_matching_word_indent(line_index = nil)
|
|
480
|
-
|
|
476
|
+
line_index ||= @editor_state.cursor_line_and_column.first
|
|
477
|
+
case editor_effective_syntax_language(line_index)
|
|
481
478
|
when :ruby
|
|
482
479
|
editor_matching_keyword_indent("end", %w[end], line_index)
|
|
483
480
|
when :lua
|
|
@@ -513,7 +510,7 @@ module Kward
|
|
|
513
510
|
next if code.empty?
|
|
514
511
|
|
|
515
512
|
stack.pop if editor_word_closer_line?(code, closers)
|
|
516
|
-
stack << line[:indent] if editor_word_opener_line?(code, opener)
|
|
513
|
+
stack << line[:indent] if editor_word_opener_line?(code, opener, line[:language])
|
|
517
514
|
end
|
|
518
515
|
stack.last || ""
|
|
519
516
|
end
|
|
@@ -532,11 +529,16 @@ module Kward
|
|
|
532
529
|
|
|
533
530
|
def editor_previous_code_lines(line_index = nil)
|
|
534
531
|
line_index ||= @editor_state.cursor_line_and_column.first
|
|
535
|
-
|
|
536
|
-
|
|
532
|
+
language = editor_effective_syntax_language(line_index)
|
|
533
|
+
first_index = editor_syntax_context_start(line_index)
|
|
534
|
+
first_index = 0 unless first_index && language != :markdown
|
|
535
|
+
|
|
536
|
+
(first_index...line_index).filter_map do |index|
|
|
537
|
+
line = @editor_state.lines[index].to_s
|
|
538
|
+
code = editor_indent_code(line, language).rstrip
|
|
537
539
|
next if code.strip.empty?
|
|
538
540
|
|
|
539
|
-
{ indent: line[/\A[ \t]*/].to_s, code: code }
|
|
541
|
+
{ indent: line[/\A[ \t]*/].to_s, code: code, language: language }
|
|
540
542
|
end
|
|
541
543
|
end
|
|
542
544
|
|
|
@@ -544,8 +546,8 @@ module Kward
|
|
|
544
546
|
code.to_s.scan(/[{}\[\]()]/)
|
|
545
547
|
end
|
|
546
548
|
|
|
547
|
-
def editor_word_opener_line?(code, opener)
|
|
548
|
-
case
|
|
549
|
+
def editor_word_opener_line?(code, opener, language)
|
|
550
|
+
case language
|
|
549
551
|
when :ruby
|
|
550
552
|
editor_ruby_line_opens_indent?(code)
|
|
551
553
|
when :lua
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require "fileutils"
|
|
2
|
-
require_relative "../../
|
|
2
|
+
require_relative "../../scratchpad_languages"
|
|
3
|
+
require_relative "runner"
|
|
3
4
|
|
|
4
5
|
# Namespace for the Kward CLI agent runtime.
|
|
5
6
|
module Kward
|
|
@@ -243,8 +244,13 @@ module Kward
|
|
|
243
244
|
end
|
|
244
245
|
|
|
245
246
|
def close_editor
|
|
247
|
+
cancel_editor_runner
|
|
246
248
|
disable_editor_mouse_reporting(force: true)
|
|
247
249
|
restore_editor_cursor_shape_locked
|
|
250
|
+
@editor_runner_state = nil
|
|
251
|
+
@editor_runner_completion = nil
|
|
252
|
+
@editor_runner_output_visible = false
|
|
253
|
+
clear_editor_runner_selection
|
|
248
254
|
@editor_text_width = nil
|
|
249
255
|
@editor_save_as_active = false
|
|
250
256
|
@editor_save_as_buffer = ""
|
|
@@ -261,12 +267,16 @@ module Kward
|
|
|
261
267
|
def handle_editor_key(key)
|
|
262
268
|
return if key.nil?
|
|
263
269
|
return handle_editor_prompt_key(key) if @editor_prompt_active
|
|
270
|
+
return handle_editor_runner_output_key(key) if editor_runner_output_visible?
|
|
264
271
|
|
|
265
272
|
reset_editor_word_completion unless editor_word_completion_tab_key?(key) && !editor_search_active?
|
|
266
273
|
mouse_result = handle_editor_mouse_key(key)
|
|
267
274
|
return mouse_result unless mouse_result == false
|
|
268
275
|
return handle_editor_save_as_key(key) if @editor_save_as_active
|
|
269
276
|
return handle_readonly_editor_key(key) if @editor_state&.readonly?
|
|
277
|
+
return cancel_editor_runner if editor_runner_running? && @editor_runner_completion && key == TerminalKeys::CTRL_C
|
|
278
|
+
super_copy_result = handle_editor_super_copy_key(key)
|
|
279
|
+
return super_copy_result unless super_copy_result == false
|
|
270
280
|
return handle_vibe_key(key) if @editor_state&.vibe?
|
|
271
281
|
return handle_emacs_key(key) if @editor_state&.emacs?
|
|
272
282
|
return handle_modern_key(key) if @editor_state&.modern?
|
|
@@ -323,6 +333,22 @@ module Kward
|
|
|
323
333
|
end
|
|
324
334
|
end
|
|
325
335
|
|
|
336
|
+
def handle_editor_super_copy_key(key)
|
|
337
|
+
sequence = parse_csi_u_key(key)
|
|
338
|
+
return false unless sequence
|
|
339
|
+
return false unless super_modifier?(sequence[:modifier]) && ctrl_code(sequence[:code]) == 99
|
|
340
|
+
|
|
341
|
+
queue_pending_keys(sequence[:remaining]) if sequence[:remaining] && !sequence[:remaining].empty?
|
|
342
|
+
return editor_search_cancel if editor_search_active?
|
|
343
|
+
return false unless editor_selection_active?
|
|
344
|
+
|
|
345
|
+
if @editor_state.vibe? && vibe_visual_mode?
|
|
346
|
+
vibe_yank_visual_selection
|
|
347
|
+
else
|
|
348
|
+
copy_editor_selection
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
|
|
326
352
|
def editor_prompt_action(instruction)
|
|
327
353
|
{
|
|
328
354
|
editor_prompt: {
|
|
@@ -1387,45 +1413,16 @@ module Kward
|
|
|
1387
1413
|
true
|
|
1388
1414
|
end
|
|
1389
1415
|
|
|
1390
|
-
def run_editor_buffer
|
|
1391
|
-
return false unless @editor_state
|
|
1392
|
-
|
|
1393
|
-
language = @editor_state.language || editor_syntax_language
|
|
1394
|
-
result = ScratchpadRunner.run(language, @editor_state.buffer)
|
|
1395
|
-
@editor_state.replace_range(0, @editor_state.buffer.length, result.buffer)
|
|
1396
|
-
@editor_state.status = "Ran #{language} (exit #{result.exit_status})"
|
|
1397
|
-
true
|
|
1398
|
-
rescue StandardError => e
|
|
1399
|
-
@editor_state.status = "Run failed: #{e.message}" if @editor_state
|
|
1400
|
-
false
|
|
1401
|
-
end
|
|
1402
|
-
|
|
1403
1416
|
def normalize_scratchpad_language(language)
|
|
1404
|
-
|
|
1405
|
-
when "", "text", "txt"
|
|
1406
|
-
:text
|
|
1407
|
-
when "markdown", "md"
|
|
1408
|
-
:markdown
|
|
1409
|
-
when "ruby", "rb"
|
|
1410
|
-
:ruby
|
|
1411
|
-
else
|
|
1412
|
-
:text
|
|
1413
|
-
end
|
|
1417
|
+
ScratchpadLanguages.normalize(language)
|
|
1414
1418
|
end
|
|
1415
1419
|
|
|
1416
1420
|
def scratchpad_display_path(language)
|
|
1417
|
-
|
|
1418
|
-
when :markdown
|
|
1419
|
-
"scratchpad.md"
|
|
1420
|
-
when :ruby
|
|
1421
|
-
"scratchpad.rb"
|
|
1422
|
-
else
|
|
1423
|
-
"scratchpad.txt"
|
|
1424
|
-
end
|
|
1421
|
+
ScratchpadLanguages.display_path(language)
|
|
1425
1422
|
end
|
|
1426
1423
|
|
|
1427
1424
|
def scratchpad_status_text(language)
|
|
1428
|
-
runnable = language
|
|
1425
|
+
runnable = ScratchpadLanguages.runnable?(language)
|
|
1429
1426
|
case current_editor_mode
|
|
1430
1427
|
when "vibe"
|
|
1431
1428
|
runnable ? "NORMAL · i insert · :w filename save · :q quit · :run run" : "NORMAL · i insert · :w filename save · :q quit"
|
|
@@ -136,7 +136,7 @@ module Kward
|
|
|
136
136
|
line_index, column = @editor_state.cursor_line_and_column
|
|
137
137
|
return nil unless editor_endwise_line_exists?(line_index)
|
|
138
138
|
|
|
139
|
-
language =
|
|
139
|
+
language = editor_effective_syntax_language(line_index)
|
|
140
140
|
definition = ENDWISE_LANGUAGES[language]
|
|
141
141
|
return nil unless definition
|
|
142
142
|
|
|
@@ -190,7 +190,8 @@ module Kward
|
|
|
190
190
|
line_index >= 0 && line_index < @editor_state.lines.length
|
|
191
191
|
end
|
|
192
192
|
|
|
193
|
-
def editor_endwise_line_opens_block?(line_index, language =
|
|
193
|
+
def editor_endwise_line_opens_block?(line_index, language = nil)
|
|
194
|
+
language ||= editor_effective_syntax_language(line_index)
|
|
194
195
|
code = editor_endwise_code_line_at(line_index, language)
|
|
195
196
|
return false if editor_endwise_ignored_definition?(code, language)
|
|
196
197
|
|
|
@@ -200,7 +201,7 @@ module Kward
|
|
|
200
201
|
end
|
|
201
202
|
|
|
202
203
|
def editor_endwise_closing_keyword_for_line(line_index, column, called_with_modifier: false)
|
|
203
|
-
language =
|
|
204
|
+
language = editor_effective_syntax_language(line_index)
|
|
204
205
|
definition = ENDWISE_LANGUAGES[language]
|
|
205
206
|
return nil unless definition
|
|
206
207
|
|
|
@@ -221,6 +222,9 @@ module Kward
|
|
|
221
222
|
return close if @editor_state.lines.length <= scan_line + 1
|
|
222
223
|
|
|
223
224
|
line_below = @editor_state.lines[scan_line + 1].to_s
|
|
225
|
+
next_language = editor_effective_syntax_language(scan_line + 1)
|
|
226
|
+
return close if next_language != language
|
|
227
|
+
|
|
224
228
|
code_below = editor_endwise_code_line_at(scan_line + 1, language)
|
|
225
229
|
closes_any_block = editor_endwise_closes_block?(code_below, language)
|
|
226
230
|
closes_this_block = editor_endwise_closes_with?(code_below, close)
|
|
@@ -254,6 +258,8 @@ module Kward
|
|
|
254
258
|
end
|
|
255
259
|
|
|
256
260
|
def editor_endwise_code_line_at(line_index, language)
|
|
261
|
+
return "" unless editor_effective_syntax_language(line_index) == language
|
|
262
|
+
|
|
257
263
|
definition = ENDWISE_LANGUAGES[language]
|
|
258
264
|
line = @editor_state.lines[line_index].to_s
|
|
259
265
|
return "" if editor_endwise_inside_block_comment?(line_index, definition.fetch(:block_comments))
|
|
@@ -263,7 +269,10 @@ module Kward
|
|
|
263
269
|
|
|
264
270
|
def editor_endwise_inside_block_comment?(line_index, block_comments)
|
|
265
271
|
active_comment = nil
|
|
266
|
-
|
|
272
|
+
first_index = editor_syntax_context_start(line_index)
|
|
273
|
+
first_index = 0 unless first_index
|
|
274
|
+
(first_index..line_index).each do |index|
|
|
275
|
+
line = @editor_state.lines[index].to_s
|
|
267
276
|
if active_comment
|
|
268
277
|
target_line = index == line_index
|
|
269
278
|
active_comment = nil if line.match?(active_comment.fetch(:end))
|