kward 0.71.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/CHANGELOG.md +41 -1
- data/Gemfile.lock +2 -2
- data/README.md +4 -0
- data/doc/agent-tools.md +15 -6
- data/doc/authentication.md +22 -1
- data/doc/code-search.md +42 -2
- data/doc/configuration.md +106 -3
- data/doc/context-budgeting.md +136 -0
- data/doc/context-tools.md +16 -3
- 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 +72 -1
- data/doc/releasing.md +37 -9
- data/doc/rpc.md +74 -4
- data/doc/session-management.md +35 -1
- data/doc/shell.md +286 -0
- data/doc/tabs.md +122 -0
- data/doc/troubleshooting.md +77 -1
- data/doc/usage.md +53 -7
- data/doc/web-search.md +12 -4
- data/doc/workspace-tools.md +51 -12
- data/examples/plugins/space_invaders.rb +377 -0
- data/lib/kward/agent.rb +1 -1
- data/lib/kward/cli/commands.rb +33 -2
- data/lib/kward/cli/git.rb +150 -0
- data/lib/kward/cli/interactive_turn.rb +73 -9
- data/lib/kward/cli/plugins.rb +54 -4
- data/lib/kward/cli/prompt_interface.rb +32 -1
- data/lib/kward/cli/runtime_helpers.rb +133 -3
- data/lib/kward/cli/sessions.rb +2 -2
- data/lib/kward/cli/settings.rb +218 -9
- data/lib/kward/cli/slash_commands.rb +415 -2
- data/lib/kward/cli/tabs.rb +695 -0
- data/lib/kward/cli.rb +158 -26
- data/lib/kward/config_files.rb +123 -1
- data/lib/kward/context_budget_meter.rb +44 -0
- data/lib/kward/conversation.rb +12 -4
- data/lib/kward/editor_mode.rb +25 -0
- data/lib/kward/ekwsh.rb +362 -0
- data/lib/kward/plugin_registry.rb +61 -0
- data/lib/kward/project_files.rb +52 -0
- data/lib/kward/prompt_history.rb +82 -0
- data/lib/kward/prompt_interface/composer_controller.rb +69 -1
- data/lib/kward/prompt_interface/composer_renderer.rb +109 -13
- 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 +387 -35
- 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/question_prompt.rb +98 -50
- data/lib/kward/prompt_interface/runtime_state.rb +43 -0
- data/lib/kward/prompt_interface/screen.rb +16 -0
- data/lib/kward/prompt_interface/selection_prompt.rb +7 -13
- data/lib/kward/prompt_interface/stream_state.rb +7 -0
- data/lib/kward/prompt_interface/transcript_buffer.rb +6 -0
- data/lib/kward/prompt_interface.rb +286 -8
- data/lib/kward/prompts/commands.rb +5 -0
- data/lib/kward/prompts.rb +2 -0
- data/lib/kward/rpc/server.rb +42 -3
- data/lib/kward/rpc/session_manager.rb +35 -47
- 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 +44 -0
- 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/text_boundary.rb +25 -0
- 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 +62 -16
- data/lib/kward/tools/tool_call.rb +10 -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 +110 -24
- data/templates/default/fulldoc/html/css/kward.css +107 -36
- data/templates/default/kward_navigation.rb +12 -1
- data/templates/default/layout/html/layout.erb +4 -2
- data/templates/default/layout/html/setup.rb +6 -0
- metadata +53 -1
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
# Namespace for the Kward CLI agent runtime.
|
|
2
|
+
module Kward
|
|
3
|
+
# Interactive terminal UI used by the CLI frontend.
|
|
4
|
+
class PromptInterface
|
|
5
|
+
# Renderer for the built-in composer file editor.
|
|
6
|
+
module EditorRenderer
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def editor_layout(width, height = screen_height)
|
|
10
|
+
content_width = [width - 4, 1].max
|
|
11
|
+
visible_count = editor_visible_line_count(height: height, width: width)
|
|
12
|
+
line_index, column = @editor_state.cursor_line_and_column
|
|
13
|
+
gutter_width = editor_line_number_gutter_width
|
|
14
|
+
text_width = editor_text_width(content_width, gutter_width)
|
|
15
|
+
sync_editor_wrap_state(text_width)
|
|
16
|
+
|
|
17
|
+
if current_editor_soft_wrap?
|
|
18
|
+
editor_wrapped_layout(width, content_width, visible_count, line_index, column, text_width)
|
|
19
|
+
else
|
|
20
|
+
editor_unwrapped_layout(width, content_width, visible_count, line_index, column, text_width)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def editor_unwrapped_layout(width, content_width, visible_count, line_index, column, text_width)
|
|
25
|
+
@editor_state.viewport_row = [[@editor_state.viewport_row, line_index - visible_count + 1].max, line_index].min
|
|
26
|
+
@editor_state.viewport_row = [@editor_state.viewport_row, 0].max
|
|
27
|
+
@editor_state.viewport_column = [[@editor_state.viewport_column.to_i, column - text_width + 1].max, column].min
|
|
28
|
+
@editor_state.viewport_column = [@editor_state.viewport_column, 0].max
|
|
29
|
+
editor_lines = @editor_state.lines
|
|
30
|
+
visible_lines = editor_lines[@editor_state.viewport_row, visible_count] || []
|
|
31
|
+
actual_visible_count = visible_lines.length
|
|
32
|
+
visible_lines << "" while visible_lines.length < visible_count
|
|
33
|
+
gutter_width = editor_line_number_gutter_width
|
|
34
|
+
rows = [editor_top_border(width)]
|
|
35
|
+
rows.concat(visible_lines.each_with_index.map do |line, index|
|
|
36
|
+
gutter = if index < actual_visible_count
|
|
37
|
+
editor_line_number_gutter(@editor_state.viewport_row + index)
|
|
38
|
+
else
|
|
39
|
+
editor_blank_line_number_gutter
|
|
40
|
+
end
|
|
41
|
+
rendered_line = editor_render_line(line, @editor_state.viewport_row + index, text_width, column_offset: @editor_state.viewport_column)
|
|
42
|
+
row = gutter + rendered_line
|
|
43
|
+
box_content_row(row, content_width)
|
|
44
|
+
end)
|
|
45
|
+
rows << footer_row(content_width, editor_status_text)
|
|
46
|
+
rows.concat(editor_bottom_rows(width))
|
|
47
|
+
cursor_row = 1 + line_index - @editor_state.viewport_row
|
|
48
|
+
cursor_col = 2 + gutter_width + [[column - @editor_state.viewport_column, 0].max, text_width - 1].min
|
|
49
|
+
[rows, cursor_row, cursor_col]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def editor_wrapped_layout(width, content_width, visible_count, line_index, column, text_width)
|
|
53
|
+
visual_rows = editor_visual_rows(text_width)
|
|
54
|
+
cursor_visual_row = editor_visual_row_for(line_index, column, text_width)
|
|
55
|
+
@editor_state.viewport_row = [[@editor_state.viewport_row, cursor_visual_row - visible_count + 1].max, cursor_visual_row].min
|
|
56
|
+
@editor_state.viewport_row = [@editor_state.viewport_row, 0].max
|
|
57
|
+
visible_rows = visual_rows[@editor_state.viewport_row, visible_count] || []
|
|
58
|
+
visible_rows << nil while visible_rows.length < visible_count
|
|
59
|
+
rows = [editor_top_border(width)]
|
|
60
|
+
rows.concat(visible_rows.map do |visual_row|
|
|
61
|
+
if visual_row
|
|
62
|
+
gutter = visual_row[:continuation] ? editor_blank_line_number_gutter : editor_line_number_gutter(visual_row[:line_index])
|
|
63
|
+
rendered_line = editor_render_line(visual_row[:line], visual_row[:line_index], text_width, column_offset: visual_row[:column_offset])
|
|
64
|
+
box_content_row(gutter + rendered_line, content_width)
|
|
65
|
+
else
|
|
66
|
+
box_content_row(editor_blank_line_number_gutter, content_width)
|
|
67
|
+
end
|
|
68
|
+
end)
|
|
69
|
+
rows << footer_row(content_width, editor_status_text)
|
|
70
|
+
rows.concat(editor_bottom_rows(width))
|
|
71
|
+
line_start = editor_visual_row_start_column(line_index, column, text_width)
|
|
72
|
+
cursor_row = 1 + cursor_visual_row - @editor_state.viewport_row
|
|
73
|
+
cursor_col = 2 + editor_line_number_gutter_width + [[column - line_start, 0].max, text_width - 1].min
|
|
74
|
+
[rows, cursor_row, cursor_col]
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def editor_visible_line_count(height: screen_height, width: screen_width)
|
|
78
|
+
visible_count = [[height - 3 - editor_bottom_rows(width).length, 1].max, 1].max
|
|
79
|
+
visible_count = [visible_count, height - 4].min if height > 4
|
|
80
|
+
[visible_count, 1].max
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def editor_bottom_rows(width)
|
|
84
|
+
@tabs.empty? ? [bottom_border(width)] : tab_border_rows(width)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def editor_render_line(line, line_index, text_width, column_offset: 0)
|
|
88
|
+
visible = line.to_s[column_offset.to_i, text_width].to_s
|
|
89
|
+
rendered = editor_render_visible_line(visible, line_index)
|
|
90
|
+
line_start = @editor_state.line_start_offset(line_index)
|
|
91
|
+
rendered = editor_overlay_line_selections(rendered, line_start, column_offset, visible.length)
|
|
92
|
+
editor_overlay_secondary_cursors(rendered, line_start, column_offset, visible.length, text_width)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def editor_overlay_line_selections(rendered, line_start, column_offset, visible_length)
|
|
96
|
+
ranges = @editor_state.selection_ranges
|
|
97
|
+
return rendered if ranges.empty?
|
|
98
|
+
|
|
99
|
+
selection_ranges = ranges.filter_map do |range|
|
|
100
|
+
selection_start = [range[0] - line_start - column_offset.to_i, 0].max
|
|
101
|
+
selection_end = [range[1] - line_start - column_offset.to_i, visible_length].min
|
|
102
|
+
[selection_start, selection_end] if selection_start < selection_end
|
|
103
|
+
end
|
|
104
|
+
return rendered if selection_ranges.empty?
|
|
105
|
+
|
|
106
|
+
editor_overlay_selection(rendered, selection_ranges)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def editor_overlay_secondary_cursors(rendered, line_start, column_offset, visible_length, text_width)
|
|
110
|
+
return rendered unless @color_enabled
|
|
111
|
+
|
|
112
|
+
cursor_columns = @editor_state.secondary_cursor_offsets.filter_map do |offset|
|
|
113
|
+
column = offset - line_start - column_offset.to_i
|
|
114
|
+
column if column >= 0 && column <= visible_length
|
|
115
|
+
end
|
|
116
|
+
return rendered if cursor_columns.empty?
|
|
117
|
+
|
|
118
|
+
if cursor_columns.include?(visible_length) && visible_length < text_width
|
|
119
|
+
rendered += " "
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
editor_overlay_selection(rendered, cursor_columns.map { |column| [column, column + 1] })
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def editor_overlay_selection(rendered, selection_ranges)
|
|
126
|
+
return rendered unless @color_enabled
|
|
127
|
+
|
|
128
|
+
output = +""
|
|
129
|
+
selected = false
|
|
130
|
+
visible_index = 0
|
|
131
|
+
index = 0
|
|
132
|
+
while index < rendered.length
|
|
133
|
+
if rendered[index] == "\e" && (match = rendered[index..].match(/\A\e\[[0-9;:]*m/))
|
|
134
|
+
output << match[0]
|
|
135
|
+
output << "\e[7m" if selected && match[0] == "\e[0m"
|
|
136
|
+
index += match[0].length
|
|
137
|
+
next
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
should_select = selection_ranges.any? { |range| visible_index >= range[0] && visible_index < range[1] }
|
|
141
|
+
if should_select != selected
|
|
142
|
+
output << (should_select ? "\e[7m" : "\e[27m")
|
|
143
|
+
selected = should_select
|
|
144
|
+
end
|
|
145
|
+
output << rendered[index]
|
|
146
|
+
visible_index += 1
|
|
147
|
+
index += 1
|
|
148
|
+
end
|
|
149
|
+
output << "\e[27m" if selected
|
|
150
|
+
output
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def editor_render_visible_line(line, line_index)
|
|
154
|
+
return editor_render_diff_line(line) if @editor_state.diff_view?
|
|
155
|
+
|
|
156
|
+
editor_highlight_line(line, line_index)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def editor_render_diff_line(line)
|
|
160
|
+
text = line.to_s
|
|
161
|
+
return colored(text, :green) if text.start_with?("+") && !text.start_with?("+++")
|
|
162
|
+
return colored(text, :red) if text.start_with?("-") && !text.start_with?("---")
|
|
163
|
+
return colored(text, :cyan) if text.start_with?("@@")
|
|
164
|
+
|
|
165
|
+
text
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def editor_line_number_gutter_width
|
|
169
|
+
[[@editor_state.lines.length.to_s.length, 4].max + 3, 1].max
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def editor_text_width(content_width, gutter_width = editor_line_number_gutter_width)
|
|
173
|
+
[content_width - gutter_width, 1].max
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def editor_visual_rows(text_width)
|
|
177
|
+
@editor_state.lines.each_with_index.flat_map do |line, line_index|
|
|
178
|
+
count = editor_visual_row_count(line, text_width)
|
|
179
|
+
count.times.map do |index|
|
|
180
|
+
column_offset = index * text_width
|
|
181
|
+
{ line_index: line_index, column_offset: column_offset, line: line, continuation: index.positive? }
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def editor_visual_row_count(line, text_width)
|
|
187
|
+
length = line.to_s.length
|
|
188
|
+
return 1 if length.zero?
|
|
189
|
+
|
|
190
|
+
((length - 1) / text_width) + 1
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def editor_visual_row_for(line_index, column, text_width)
|
|
194
|
+
before = @editor_state.lines.first(line_index).sum { |line| editor_visual_row_count(line, text_width) }
|
|
195
|
+
before + (editor_visual_row_start_column(line_index, column, text_width) / text_width)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def editor_visual_row_start_column(line_index, column, text_width)
|
|
199
|
+
line = @editor_state.lines[line_index].to_s
|
|
200
|
+
return 0 if column.to_i.zero?
|
|
201
|
+
return column.to_i - text_width if column.to_i == line.length && (column.to_i % text_width).zero?
|
|
202
|
+
|
|
203
|
+
(column.to_i / text_width) * text_width
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def editor_line_number_gutter(line_index)
|
|
207
|
+
number = editor_display_line_number(line_index).to_s.rjust(editor_line_number_gutter_width - 3)
|
|
208
|
+
colored("#{number} │ ", :dark_forest_green)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def editor_display_line_number(line_index)
|
|
212
|
+
return line_index + 1 unless current_editor_line_numbers == "relative"
|
|
213
|
+
return line_index + 1 if @editor_state.readonly?
|
|
214
|
+
|
|
215
|
+
cursor_line, = @editor_state.cursor_line_and_column
|
|
216
|
+
line_index == cursor_line ? line_index + 1 : (line_index - cursor_line).abs
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def editor_blank_line_number_gutter
|
|
220
|
+
colored(" " * editor_line_number_gutter_width, :dark_forest_green)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def editor_top_border(width)
|
|
224
|
+
title_prefix = @editor_state.diff_view? ? "Diff" : "Edit"
|
|
225
|
+
dirty_marker = @editor_state.dirty? && !@editor_state.readonly? ? " *" : ""
|
|
226
|
+
title = visible_truncate("#{title_prefix} #{editor_display_path}#{dirty_marker}", [width - 4, 1].max)
|
|
227
|
+
plain_title = ANSI.strip(title)
|
|
228
|
+
"#{colored("╭", :primary_green)} #{title} #{colored("─" * [width - plain_title.length - 4, 0].max, :primary_green)}#{colored("╮", :primary_green)}"
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def editor_display_path
|
|
232
|
+
Pathname.new(@editor_state.path).relative_path_from(Pathname.new(Dir.pwd)).to_s
|
|
233
|
+
rescue StandardError
|
|
234
|
+
@editor_state.path
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def editor_status_text
|
|
238
|
+
text = @editor_state.search_active ? "#{@editor_state.search_direction == :backward ? "Search backward" : "Search"}: #{@editor_state.search_query}" : @editor_state.status
|
|
239
|
+
visible_truncate(text, [screen_width - 4, 1].max)
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Namespace for the Kward CLI agent runtime.
|
|
2
|
+
module Kward
|
|
3
|
+
# Interactive terminal UI used by the CLI frontend.
|
|
4
|
+
class PromptInterface
|
|
5
|
+
# Incremental search state and operations for editor buffers.
|
|
6
|
+
class EditorSearch
|
|
7
|
+
attr_reader :query, :direction
|
|
8
|
+
|
|
9
|
+
def initialize(direction: :forward)
|
|
10
|
+
@active = false
|
|
11
|
+
@query = +""
|
|
12
|
+
@direction = direction
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def active?
|
|
16
|
+
@active == true
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def begin(direction = :forward)
|
|
20
|
+
@active = true
|
|
21
|
+
@direction = direction
|
|
22
|
+
@query = +""
|
|
23
|
+
status_prefix
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def cancel
|
|
27
|
+
@active = false
|
|
28
|
+
"Search cancelled"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def append(text)
|
|
32
|
+
@query << text.to_s
|
|
33
|
+
"#{status_prefix} #{@query}"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def delete_character
|
|
37
|
+
@query = @query[0...-1].to_s
|
|
38
|
+
"#{status_prefix} #{@query}"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def confirm(buffer:, cursor:)
|
|
42
|
+
confirmed_query = @query.to_s
|
|
43
|
+
@active = false
|
|
44
|
+
return { status: "Search cancelled", found: false } if confirmed_query.empty?
|
|
45
|
+
|
|
46
|
+
repeat(buffer: buffer, cursor: cursor, direction: @direction, query: confirmed_query)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def repeat(buffer:, cursor:, direction: @direction, query: @query)
|
|
50
|
+
query = query.to_s
|
|
51
|
+
return { status: "No previous search", found: false } if query.empty?
|
|
52
|
+
|
|
53
|
+
@query = query
|
|
54
|
+
@direction = direction
|
|
55
|
+
index = if direction == :backward
|
|
56
|
+
search_from = cursor.positive? ? cursor - 1 : buffer.length
|
|
57
|
+
buffer.rindex(query, search_from) || buffer.rindex(query)
|
|
58
|
+
else
|
|
59
|
+
buffer.index(query, cursor + 1) || buffer.index(query)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
if index
|
|
63
|
+
{ cursor: index, status: "Found: #{query}", found: true }
|
|
64
|
+
else
|
|
65
|
+
{ status: "No match: #{query}", found: false }
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def status_prefix
|
|
72
|
+
@direction == :backward ? "Search backward:" : "Search:"
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# Namespace for the Kward CLI agent runtime.
|
|
2
|
+
module Kward
|
|
3
|
+
# Interactive terminal UI used by the CLI frontend.
|
|
4
|
+
class PromptInterface
|
|
5
|
+
# Primary and secondary selection/cursor storage for editor buffers.
|
|
6
|
+
class EditorSelections
|
|
7
|
+
attr_reader :anchor, :secondary
|
|
8
|
+
|
|
9
|
+
def initialize(cursor:, buffer_length:, anchor: nil, secondary: [])
|
|
10
|
+
@cursor = cursor
|
|
11
|
+
@buffer_length = buffer_length
|
|
12
|
+
@anchor = anchor.nil? ? nil : clamp_offset(anchor)
|
|
13
|
+
@secondary = secondary.map { |selection| normalized_selection(selection) }
|
|
14
|
+
normalize
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def cursor=(value)
|
|
18
|
+
@cursor = value
|
|
19
|
+
normalize
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def buffer_length=(value)
|
|
23
|
+
@buffer_length = value.to_i
|
|
24
|
+
@anchor = clamp_offset(@anchor) unless @anchor.nil?
|
|
25
|
+
@secondary = @secondary.map { |selection| normalized_selection(selection) }
|
|
26
|
+
normalize
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def anchor=(value)
|
|
30
|
+
@anchor = value.nil? ? nil : clamp_offset(value)
|
|
31
|
+
normalize
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def all
|
|
35
|
+
normalize
|
|
36
|
+
[primary] + @secondary.map(&:dup)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def multi_cursor?
|
|
40
|
+
normalize
|
|
41
|
+
@secondary.any?
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def set(values)
|
|
45
|
+
first, *rest = values.to_a
|
|
46
|
+
if first
|
|
47
|
+
@anchor = first[:anchor]
|
|
48
|
+
@cursor = first[:cursor]
|
|
49
|
+
else
|
|
50
|
+
@anchor = nil
|
|
51
|
+
@cursor = 0
|
|
52
|
+
end
|
|
53
|
+
@secondary = rest.map { |selection| normalized_selection(selection) }
|
|
54
|
+
normalize
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def add(anchor, cursor = anchor)
|
|
58
|
+
@secondary << normalized_selection(anchor: anchor, cursor: cursor)
|
|
59
|
+
normalize
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def clear
|
|
63
|
+
@anchor = nil
|
|
64
|
+
@secondary = []
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def collapse_to_primary
|
|
68
|
+
@secondary = []
|
|
69
|
+
@anchor = nil
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def secondary_cursor_offsets
|
|
73
|
+
normalize
|
|
74
|
+
@secondary.filter_map do |selection|
|
|
75
|
+
selection[:cursor] if selection[:anchor] == selection[:cursor]
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def primary
|
|
80
|
+
{ anchor: @anchor || @cursor, cursor: @cursor }
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def primary_active?(vibe_visual: false)
|
|
84
|
+
return false if @anchor.nil?
|
|
85
|
+
return true if vibe_visual
|
|
86
|
+
|
|
87
|
+
@anchor != @cursor
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def range_for(selection)
|
|
91
|
+
[selection[:anchor], selection[:cursor]].minmax
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
def normalize
|
|
97
|
+
seen = { [primary[:anchor], primary[:cursor]] => true }
|
|
98
|
+
@secondary = @secondary.filter_map do |selection|
|
|
99
|
+
normalized = normalized_selection(selection)
|
|
100
|
+
key = [normalized[:anchor], normalized[:cursor]]
|
|
101
|
+
next if seen[key]
|
|
102
|
+
|
|
103
|
+
seen[key] = true
|
|
104
|
+
normalized
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def normalized_selection(selection)
|
|
109
|
+
{
|
|
110
|
+
anchor: clamp_offset(selection[:anchor]),
|
|
111
|
+
cursor: clamp_offset(selection[:cursor])
|
|
112
|
+
}
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def clamp_offset(value)
|
|
116
|
+
[[value.to_i, 0].max, @buffer_length].min
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|