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
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
require_relative "../../scratchpad_runner"
|
|
2
|
+
require_relative "runner_state"
|
|
3
|
+
|
|
4
|
+
# Namespace for the Kward CLI agent runtime.
|
|
5
|
+
module Kward
|
|
6
|
+
# Coordinates execution and output viewing for editor buffers.
|
|
7
|
+
class PromptInterface
|
|
8
|
+
module EditorRunner
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def editor_runner_config
|
|
12
|
+
config = @editor_runners_source.respond_to?(:call) ? @editor_runners_source.call : {}
|
|
13
|
+
config.is_a?(Hash) ? config : {}
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def editor_runner_output_visible?
|
|
17
|
+
@editor_runner_output_visible && @editor_runner_state
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def editor_runner_running?
|
|
21
|
+
@editor_runner_state&.running?
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def run_editor_buffer(source: nil, language: nil, source_path: nil, output_visible: true, completion: nil)
|
|
25
|
+
return false unless @editor_state
|
|
26
|
+
return false if @editor_state.readonly?
|
|
27
|
+
if editor_runner_running?
|
|
28
|
+
@editor_runner_output_visible = true
|
|
29
|
+
@editor_state.status = "Runner already active · Ctrl+C cancel"
|
|
30
|
+
return false
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
language ||= @editor_state.language || editor_syntax_language
|
|
34
|
+
unless ScratchpadLanguages.runnable?(language)
|
|
35
|
+
@editor_state.status = "No runner available for #{language || "text"}"
|
|
36
|
+
return false
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
source = source.nil? ? @editor_state.buffer.dup : source.to_s
|
|
40
|
+
source_path ||= @editor_state.path
|
|
41
|
+
workspace_root = prompt_workspace_root
|
|
42
|
+
state = EditorRunnerState.new(language: language, source: source)
|
|
43
|
+
@editor_runner_state = state
|
|
44
|
+
@editor_runner_completion = completion
|
|
45
|
+
@editor_runner_output_visible = output_visible
|
|
46
|
+
clear_editor_runner_selection
|
|
47
|
+
@editor_state.status = "Running #{language} · Ctrl+C cancel"
|
|
48
|
+
Thread.new do
|
|
49
|
+
Thread.current.report_on_exception = false
|
|
50
|
+
result = ScratchpadRunner.run(
|
|
51
|
+
language,
|
|
52
|
+
source,
|
|
53
|
+
cancelled: state.method(:cancel_requested?),
|
|
54
|
+
cwd: workspace_root,
|
|
55
|
+
source_path: source_path,
|
|
56
|
+
runner_config: editor_runner_config
|
|
57
|
+
)
|
|
58
|
+
finish_editor_runner(state, result)
|
|
59
|
+
rescue StandardError => e
|
|
60
|
+
fail_editor_runner(state, e)
|
|
61
|
+
end
|
|
62
|
+
true
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def finish_editor_runner(state, result)
|
|
66
|
+
@mutex.synchronize do
|
|
67
|
+
return unless @editor_runner_state.equal?(state) && @editor_state
|
|
68
|
+
|
|
69
|
+
state.complete(result)
|
|
70
|
+
completion = @editor_runner_completion
|
|
71
|
+
@editor_runner_completion = nil
|
|
72
|
+
@editor_runner_output_visible = false if completion
|
|
73
|
+
completion ? completion.call(result, nil) : @editor_state.status = editor_runner_result_status(result)
|
|
74
|
+
render_prompt_locked if @started && @asking
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def fail_editor_runner(state, error)
|
|
79
|
+
@mutex.synchronize do
|
|
80
|
+
return unless @editor_runner_state.equal?(state) && @editor_state
|
|
81
|
+
|
|
82
|
+
state.fail(error)
|
|
83
|
+
completion = @editor_runner_completion
|
|
84
|
+
@editor_runner_completion = nil
|
|
85
|
+
@editor_runner_output_visible = false if completion
|
|
86
|
+
if completion
|
|
87
|
+
completion.call(nil, error)
|
|
88
|
+
else
|
|
89
|
+
@editor_state.status = "Run failed: #{error.message}"
|
|
90
|
+
end
|
|
91
|
+
render_prompt_locked if @started && @asking
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def editor_runner_result_status(result)
|
|
96
|
+
return "Canceled · Esc close · Ctrl+R rerun" if result.cancelled
|
|
97
|
+
|
|
98
|
+
"Exit #{result.exit_status} · #{format_runner_duration(result.duration)} · Esc close · Ctrl+R rerun"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def format_runner_duration(duration)
|
|
102
|
+
format("%.2fs", duration.to_f)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def cancel_editor_runner
|
|
106
|
+
return false unless editor_runner_running?
|
|
107
|
+
|
|
108
|
+
@editor_runner_state.cancel
|
|
109
|
+
@editor_state.status = "Canceling #{@editor_runner_state.language} · please wait"
|
|
110
|
+
true
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def close_editor_runner_output
|
|
114
|
+
@editor_runner_output_visible = false
|
|
115
|
+
clear_editor_runner_selection
|
|
116
|
+
true
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def handle_editor_runner_output_key(key)
|
|
120
|
+
mouse_result = handle_editor_runner_mouse_key(key)
|
|
121
|
+
return mouse_result unless mouse_result == false
|
|
122
|
+
return true if handle_bundled_key(key) { |token| handle_editor_runner_output_key(token) }
|
|
123
|
+
|
|
124
|
+
if (sequence = parse_csi_u_key(key))
|
|
125
|
+
queue_pending_keys(sequence[:remaining]) if sequence[:remaining] && !sequence[:remaining].empty?
|
|
126
|
+
if sequence[:code] == 121 && !ctrl_modifier?(sequence[:modifier]) && @editor_state&.vibe? && editor_runner_selection_active?
|
|
127
|
+
return copy_editor_runner_selection
|
|
128
|
+
end
|
|
129
|
+
normalized_code = ctrl_code(sequence[:code])
|
|
130
|
+
if (ctrl_modifier?(sequence[:modifier]) || super_modifier?(sequence[:modifier])) && normalized_code == 99
|
|
131
|
+
return copy_editor_runner_selection if editor_runner_selection_active?
|
|
132
|
+
return cancel_editor_runner if ctrl_modifier?(sequence[:modifier]) && editor_runner_running?
|
|
133
|
+
end
|
|
134
|
+
if ctrl_modifier?(sequence[:modifier])
|
|
135
|
+
return run_editor_buffer if normalized_code == 114
|
|
136
|
+
if normalized_code == 113
|
|
137
|
+
close_editor_runner_output
|
|
138
|
+
return quit_editor
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
return close_editor_runner_output if sequence[:code] == 27
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
key_name = key_name_for(key)
|
|
145
|
+
case key_name
|
|
146
|
+
when :up
|
|
147
|
+
scroll_editor_runner_output(-1)
|
|
148
|
+
when :down
|
|
149
|
+
scroll_editor_runner_output(1)
|
|
150
|
+
when :pageup
|
|
151
|
+
scroll_editor_runner_output(-editor_runner_output_page_size)
|
|
152
|
+
when :pagedown
|
|
153
|
+
scroll_editor_runner_output(editor_runner_output_page_size)
|
|
154
|
+
when :ctrl_q
|
|
155
|
+
close_editor_runner_output
|
|
156
|
+
quit_editor
|
|
157
|
+
when :ctrl_r
|
|
158
|
+
run_editor_buffer
|
|
159
|
+
when :ctrl_c
|
|
160
|
+
if editor_runner_selection_active?
|
|
161
|
+
copy_editor_runner_selection
|
|
162
|
+
else
|
|
163
|
+
editor_runner_running? ? cancel_editor_runner : close_editor_runner_output
|
|
164
|
+
end
|
|
165
|
+
else
|
|
166
|
+
case key
|
|
167
|
+
when "\e", "q", "Q"
|
|
168
|
+
close_editor_runner_output
|
|
169
|
+
when "y"
|
|
170
|
+
copy_editor_runner_selection if @editor_state&.vibe? && editor_runner_selection_active?
|
|
171
|
+
when TerminalKeys::CTRL_Q
|
|
172
|
+
close_editor_runner_output
|
|
173
|
+
quit_editor
|
|
174
|
+
when TerminalKeys::CTRL_R
|
|
175
|
+
run_editor_buffer
|
|
176
|
+
when TerminalKeys::CTRL_C
|
|
177
|
+
if editor_runner_selection_active?
|
|
178
|
+
copy_editor_runner_selection
|
|
179
|
+
else
|
|
180
|
+
editor_runner_running? ? cancel_editor_runner : close_editor_runner_output
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
true
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def handle_editor_runner_mouse_key(key)
|
|
188
|
+
event = parse_sgr_mouse_event(key)
|
|
189
|
+
return false unless event
|
|
190
|
+
|
|
191
|
+
queue_pending_keys(event[:remaining]) unless event[:remaining].empty?
|
|
192
|
+
case event[:code]
|
|
193
|
+
when 64
|
|
194
|
+
scroll_editor_runner_output(-1)
|
|
195
|
+
when 65
|
|
196
|
+
scroll_editor_runner_output(1)
|
|
197
|
+
else
|
|
198
|
+
if event[:drag]
|
|
199
|
+
update_editor_runner_selection(editor_runner_position_for_mouse_event(event))
|
|
200
|
+
elsif event[:button].zero?
|
|
201
|
+
event[:release] ? finish_editor_runner_selection : begin_editor_runner_selection(event)
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
true
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def begin_editor_runner_selection(event)
|
|
208
|
+
position = editor_runner_position_for_mouse_event(event)
|
|
209
|
+
return true unless position
|
|
210
|
+
|
|
211
|
+
click_count = editor_runner_mouse_click_count(event)
|
|
212
|
+
case click_count
|
|
213
|
+
when 3..Float::INFINITY
|
|
214
|
+
@editor_runner_selection_anchor = [position[0], 0]
|
|
215
|
+
@editor_runner_selection_cursor = [position[0], editor_runner_line(position[0]).length]
|
|
216
|
+
when 2
|
|
217
|
+
range = editor_runner_word_range_at(position)
|
|
218
|
+
if range
|
|
219
|
+
@editor_runner_selection_anchor = [position[0], range[0]]
|
|
220
|
+
@editor_runner_selection_cursor = [position[0], range[1]]
|
|
221
|
+
else
|
|
222
|
+
@editor_runner_selection_anchor = position
|
|
223
|
+
@editor_runner_selection_cursor = position
|
|
224
|
+
end
|
|
225
|
+
else
|
|
226
|
+
@editor_runner_selection_anchor = position
|
|
227
|
+
@editor_runner_selection_cursor = position
|
|
228
|
+
end
|
|
229
|
+
@editor_runner_mouse_dragging = true
|
|
230
|
+
true
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def update_editor_runner_selection(position)
|
|
234
|
+
return true unless @editor_runner_mouse_dragging && position
|
|
235
|
+
|
|
236
|
+
@editor_runner_selection_cursor = position
|
|
237
|
+
true
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def finish_editor_runner_selection
|
|
241
|
+
@editor_runner_mouse_dragging = false
|
|
242
|
+
true
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def editor_runner_mouse_click_count(event)
|
|
246
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
247
|
+
count = if @editor_runner_last_click && now - @editor_runner_last_click[:time] <= 0.5 &&
|
|
248
|
+
(@editor_runner_last_click[:column] - event[:column]).abs <= 1 &&
|
|
249
|
+
(@editor_runner_last_click[:row] - event[:row]).abs <= 1
|
|
250
|
+
@editor_runner_last_click[:count] + 1
|
|
251
|
+
else
|
|
252
|
+
1
|
|
253
|
+
end
|
|
254
|
+
@editor_runner_last_click = { time: now, column: event[:column], row: event[:row], count: count }
|
|
255
|
+
count
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def editor_runner_position_for_mouse_event(event)
|
|
259
|
+
row_offset = event[:row] - editor_runner_output_content_top_row
|
|
260
|
+
return nil if row_offset.negative? || row_offset >= editor_runner_output_visible_count
|
|
261
|
+
|
|
262
|
+
line_index = @editor_runner_state.scroll_row + row_offset
|
|
263
|
+
line = editor_runner_line(line_index)
|
|
264
|
+
return nil unless line
|
|
265
|
+
|
|
266
|
+
column = [[event[:column] - 3, 0].max, line.length].min
|
|
267
|
+
[line_index, column]
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def editor_runner_output_content_top_row
|
|
271
|
+
height = screen_height
|
|
272
|
+
source_count, = editor_split_visible_counts(height: height, width: screen_width)
|
|
273
|
+
composer_top_row(height) + source_count + 3
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def editor_runner_line(line_index)
|
|
277
|
+
lines = editor_runner_output_lines([screen_width - 4, 1].max)
|
|
278
|
+
line = lines[line_index]
|
|
279
|
+
line && ANSI.strip(line)
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def editor_runner_selection_active?
|
|
283
|
+
@editor_runner_selection_anchor && @editor_runner_selection_cursor &&
|
|
284
|
+
@editor_runner_selection_anchor != @editor_runner_selection_cursor
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def clear_editor_runner_selection
|
|
288
|
+
@editor_runner_selection_anchor = nil
|
|
289
|
+
@editor_runner_selection_cursor = nil
|
|
290
|
+
@editor_runner_mouse_dragging = false
|
|
291
|
+
@editor_runner_last_click = nil
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def editor_runner_selection_bounds
|
|
295
|
+
return nil unless editor_runner_selection_active?
|
|
296
|
+
|
|
297
|
+
[@editor_runner_selection_anchor, @editor_runner_selection_cursor].minmax_by { |position| position }
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def editor_runner_selection_range_for(line_index, line_length)
|
|
301
|
+
bounds = editor_runner_selection_bounds
|
|
302
|
+
return nil unless bounds
|
|
303
|
+
|
|
304
|
+
first, last = bounds
|
|
305
|
+
return nil if line_index < first[0] || line_index > last[0]
|
|
306
|
+
|
|
307
|
+
start_column = line_index == first[0] ? first[1] : 0
|
|
308
|
+
end_column = line_index == last[0] ? last[1] : line_length
|
|
309
|
+
[start_column, end_column] if start_column < end_column
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def editor_runner_word_range_at(position)
|
|
313
|
+
line = editor_runner_line(position[0]).to_s
|
|
314
|
+
return nil if line.empty? || TextBoundary.word_separator?(line[[position[1], line.length - 1].min])
|
|
315
|
+
|
|
316
|
+
index = [position[1], line.length - 1].min
|
|
317
|
+
start_column = index
|
|
318
|
+
start_column -= 1 while start_column.positive? && !TextBoundary.word_separator?(line[start_column - 1])
|
|
319
|
+
end_column = index + 1
|
|
320
|
+
end_column += 1 while end_column < line.length && !TextBoundary.word_separator?(line[end_column])
|
|
321
|
+
[start_column, end_column]
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def editor_runner_selected_text
|
|
325
|
+
bounds = editor_runner_selection_bounds
|
|
326
|
+
return "" unless bounds
|
|
327
|
+
|
|
328
|
+
first, last = bounds
|
|
329
|
+
lines = (first[0]..last[0]).map { |line_index| editor_runner_line(line_index).to_s }
|
|
330
|
+
if lines.length == 1
|
|
331
|
+
lines[0] = lines[0][first[1]...last[1]] || ""
|
|
332
|
+
else
|
|
333
|
+
lines[0] = lines[0][first[1]..] || ""
|
|
334
|
+
lines[-1] = lines[-1][...last[1]] || ""
|
|
335
|
+
end
|
|
336
|
+
lines.join("\n")
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def copy_editor_runner_selection
|
|
340
|
+
text = editor_runner_selected_text
|
|
341
|
+
return false if text.empty?
|
|
342
|
+
|
|
343
|
+
print_output_locked(TerminalSequences.osc52(text))
|
|
344
|
+
flush_output_locked if @output_io.respond_to?(:flush)
|
|
345
|
+
clear_editor_runner_selection
|
|
346
|
+
@editor_state.status = "Copied selection"
|
|
347
|
+
true
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def scroll_editor_runner_output(delta)
|
|
351
|
+
return false unless @editor_runner_state
|
|
352
|
+
|
|
353
|
+
@editor_runner_state.scroll_by(delta, maximum: editor_runner_output_scroll_max)
|
|
354
|
+
true
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
def editor_runner_output_page_size
|
|
358
|
+
[editor_runner_output_visible_count, 1].max
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
end
|
|
362
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
require "digest"
|
|
2
|
+
|
|
3
|
+
# Namespace for the Kward CLI agent runtime.
|
|
4
|
+
module Kward
|
|
5
|
+
# Ephemeral state for the output of an editor runner.
|
|
6
|
+
class PromptInterface
|
|
7
|
+
class EditorRunnerState
|
|
8
|
+
attr_reader :language, :source_digest
|
|
9
|
+
|
|
10
|
+
def initialize(language:, source:)
|
|
11
|
+
@language = language.to_sym
|
|
12
|
+
@source_digest = Digest::SHA256.hexdigest(source.to_s)
|
|
13
|
+
@mutex = Mutex.new
|
|
14
|
+
@running = true
|
|
15
|
+
@cancel_requested = false
|
|
16
|
+
@result = nil
|
|
17
|
+
@error = nil
|
|
18
|
+
@scroll_row = 0
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def running?
|
|
22
|
+
@mutex.synchronize { @running }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def cancel_requested?
|
|
26
|
+
@mutex.synchronize { @cancel_requested }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def cancel
|
|
30
|
+
@mutex.synchronize { @cancel_requested = true }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def complete(result)
|
|
34
|
+
@mutex.synchronize do
|
|
35
|
+
@result = result
|
|
36
|
+
@running = false
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def fail(error)
|
|
41
|
+
@mutex.synchronize do
|
|
42
|
+
@error = error
|
|
43
|
+
@running = false
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def result
|
|
48
|
+
@mutex.synchronize { @result }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def error
|
|
52
|
+
@mutex.synchronize { @error }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def output_text
|
|
56
|
+
@mutex.synchronize do
|
|
57
|
+
return @error.message.to_s if @error
|
|
58
|
+
|
|
59
|
+
@result&.output.to_s
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def scroll_row
|
|
64
|
+
@mutex.synchronize { @scroll_row }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def set_scroll_row(row, maximum:)
|
|
68
|
+
@mutex.synchronize do
|
|
69
|
+
@scroll_row = [[row.to_i, 0].max, maximum.to_i].min
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def scroll_by(delta, maximum:)
|
|
74
|
+
set_scroll_row(scroll_row + delta.to_i, maximum: maximum)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -180,6 +180,15 @@ module Kward
|
|
|
180
180
|
sync_vibe_state
|
|
181
181
|
end
|
|
182
182
|
|
|
183
|
+
def vibe_command_range
|
|
184
|
+
@vibe_state.command_range
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def vibe_command_range=(value)
|
|
188
|
+
@vibe_state.command_range = value
|
|
189
|
+
sync_vibe_state
|
|
190
|
+
end
|
|
191
|
+
|
|
183
192
|
def vibe_last_change
|
|
184
193
|
@vibe_state.last_change
|
|
185
194
|
end
|
|
@@ -207,15 +216,6 @@ module Kward
|
|
|
207
216
|
sync_vibe_state
|
|
208
217
|
end
|
|
209
218
|
|
|
210
|
-
def vibe_visual_block_insert
|
|
211
|
-
@vibe_state.visual_block_insert
|
|
212
|
-
end
|
|
213
|
-
|
|
214
|
-
def vibe_visual_block_insert=(value)
|
|
215
|
-
@vibe_state.visual_block_insert = value
|
|
216
|
-
sync_vibe_state
|
|
217
|
-
end
|
|
218
|
-
|
|
219
219
|
def vibe_marks
|
|
220
220
|
@vibe_state.marks
|
|
221
221
|
end
|
|
@@ -1289,10 +1289,10 @@ module Kward
|
|
|
1289
1289
|
@vibe_mode = @vibe_state.mode
|
|
1290
1290
|
@vibe_pending = @vibe_state.pending
|
|
1291
1291
|
@vibe_command = @vibe_state.command
|
|
1292
|
+
@vibe_command_range = @vibe_state.command_range
|
|
1292
1293
|
@vibe_last_change = @vibe_state.last_change
|
|
1293
1294
|
@vibe_last_find = @vibe_state.last_find
|
|
1294
1295
|
@vibe_last_visual_selection = @vibe_state.last_visual_selection
|
|
1295
|
-
@vibe_visual_block_insert = @vibe_state.visual_block_insert
|
|
1296
1296
|
@vibe_marks = @vibe_state.marks
|
|
1297
1297
|
@vibe_registers = @vibe_state.registers
|
|
1298
1298
|
@vibe_register_types = @vibe_state.register_types
|
|
@@ -53,6 +53,7 @@ module Kward
|
|
|
53
53
|
keywords: %w[begin if while for try let quote function macro module baremodule struct mutable abstract primitive type do end else elseif catch finally true false nothing]
|
|
54
54
|
},
|
|
55
55
|
makefile: {
|
|
56
|
+
extensions: %w[.mk],
|
|
56
57
|
filenames: %w[Makefile makefile GNUmakefile],
|
|
57
58
|
line_comment: "#",
|
|
58
59
|
keywords: %w[ifeq ifneq ifdef ifndef else endif include define endef export unexport override]
|
|
@@ -125,11 +126,17 @@ module Kward
|
|
|
125
126
|
def editor_highlight_line(line, line_index = nil)
|
|
126
127
|
return line.to_s unless @color_enabled
|
|
127
128
|
|
|
128
|
-
|
|
129
|
+
if editor_syntax_language == :markdown
|
|
130
|
+
editor_highlight_markdown(line, line_index)
|
|
131
|
+
else
|
|
132
|
+
editor_highlight_language_line(line, editor_syntax_language, line_index)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def editor_highlight_language_line(line, language, line_index = nil)
|
|
137
|
+
case language
|
|
129
138
|
when :ruby
|
|
130
139
|
editor_highlight_ruby(line, line_index)
|
|
131
|
-
when :markdown
|
|
132
|
-
editor_highlight_markdown(line)
|
|
133
140
|
when :json
|
|
134
141
|
editor_highlight_json(line)
|
|
135
142
|
when :yaml
|
|
@@ -143,7 +150,7 @@ module Kward
|
|
|
143
150
|
when :sql
|
|
144
151
|
editor_highlight_sql(line)
|
|
145
152
|
else
|
|
146
|
-
editor_highlight_generic(line,
|
|
153
|
+
editor_highlight_generic(line, language, line_index)
|
|
147
154
|
end
|
|
148
155
|
end
|
|
149
156
|
|
|
@@ -395,16 +402,71 @@ module Kward
|
|
|
395
402
|
nil
|
|
396
403
|
end
|
|
397
404
|
|
|
398
|
-
def editor_highlight_markdown(line)
|
|
405
|
+
def editor_highlight_markdown(line, line_index = nil)
|
|
399
406
|
text = line.to_s
|
|
407
|
+
fence_context = editor_markdown_fence_context(line_index)
|
|
408
|
+
return editor_highlight_markdown_fence(text) if fence_context == :marker
|
|
409
|
+
if fence_context.is_a?(Array)
|
|
410
|
+
language, nested_line_index = fence_context
|
|
411
|
+
return editor_highlight_language_line(text, language, nested_line_index) if language
|
|
412
|
+
|
|
413
|
+
return text
|
|
414
|
+
end
|
|
415
|
+
|
|
400
416
|
return editor_highlight_markdown_heading(text) if text.match?(/\A\s{0,3}[#]{1,6}\s/)
|
|
401
|
-
return editor_highlight_markdown_fence(text) if text.match?(/\A\s*```/)
|
|
402
417
|
return editor_highlight_markdown_blockquote(text) if text.match?(/\A\s*>/)
|
|
403
418
|
return editor_highlight_markdown_list(text) if text.match?(/\A\s*(?:[-*+]\s+|\d+\.\s+)/)
|
|
404
419
|
|
|
405
420
|
editor_highlight_markdown_inline(text)
|
|
406
421
|
end
|
|
407
422
|
|
|
423
|
+
def editor_markdown_fence_context(line_index)
|
|
424
|
+
return nil unless line_index && @editor_state
|
|
425
|
+
|
|
426
|
+
inside_fence = false
|
|
427
|
+
language = nil
|
|
428
|
+
opening_index = nil
|
|
429
|
+
@editor_state.lines.first(line_index.to_i + 1).each_with_index do |line, index|
|
|
430
|
+
match = line.to_s.match(/\A\s*```([^`]*)\s*\z/)
|
|
431
|
+
if match
|
|
432
|
+
if inside_fence
|
|
433
|
+
inside_fence = false
|
|
434
|
+
language = nil
|
|
435
|
+
opening_index = nil
|
|
436
|
+
else
|
|
437
|
+
inside_fence = true
|
|
438
|
+
language = editor_markdown_fence_language(match[1])
|
|
439
|
+
opening_index = index
|
|
440
|
+
end
|
|
441
|
+
return :marker if index == line_index
|
|
442
|
+
elsif inside_fence && index == line_index
|
|
443
|
+
return [language, index, opening_index]
|
|
444
|
+
end
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
nil
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
def editor_effective_syntax_language(line_index = nil)
|
|
451
|
+
language = editor_syntax_language
|
|
452
|
+
return language unless language == :markdown
|
|
453
|
+
|
|
454
|
+
context = editor_markdown_fence_context(line_index)
|
|
455
|
+
context.is_a?(Array) ? context.first || :markdown : :markdown
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
def editor_syntax_context_start(line_index = nil)
|
|
459
|
+
return nil unless editor_syntax_language == :markdown
|
|
460
|
+
|
|
461
|
+
context = editor_markdown_fence_context(line_index)
|
|
462
|
+
context.is_a?(Array) ? context[2] : nil
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
def editor_markdown_fence_language(info)
|
|
466
|
+
name = info.to_s.strip.split(/\s/, 2).first.to_s.downcase
|
|
467
|
+
ScratchpadLanguages::ALIASES[name]
|
|
468
|
+
end
|
|
469
|
+
|
|
408
470
|
def editor_highlight_markdown_heading(line)
|
|
409
471
|
line.sub(/\A(\s{0,3}[#]{1,6}\s+)(.*)\z/) do
|
|
410
472
|
"#{colored(Regexp.last_match(1), :cyan)}#{colored(Regexp.last_match(2), :bold)}"
|
|
@@ -5,7 +5,7 @@ module Kward
|
|
|
5
5
|
# Modal Vibe editor state for one editor buffer.
|
|
6
6
|
class VibeEditorState
|
|
7
7
|
attr_accessor :mode, :pending, :command, :last_change, :last_find
|
|
8
|
-
attr_accessor :
|
|
8
|
+
attr_accessor :command_range, :last_visual_selection
|
|
9
9
|
attr_accessor :marks, :registers, :register_types, :macros, :recording_macro, :last_macro
|
|
10
10
|
attr_accessor :kill_linewise, :previous_change_cursor, :jump_back_list, :jump_forward_list
|
|
11
11
|
|
|
@@ -13,10 +13,10 @@ module Kward
|
|
|
13
13
|
@mode = editor_mode == "vibe" ? "normal" : nil
|
|
14
14
|
@pending = ""
|
|
15
15
|
@command = ""
|
|
16
|
+
@command_range = nil
|
|
16
17
|
@last_change = nil
|
|
17
18
|
@last_find = nil
|
|
18
19
|
@last_visual_selection = nil
|
|
19
|
-
@visual_block_insert = nil
|
|
20
20
|
@marks = {}
|
|
21
21
|
@registers = {}
|
|
22
22
|
@register_types = {}
|
|
@@ -34,10 +34,10 @@ module Kward
|
|
|
34
34
|
state.mode = other.mode&.dup
|
|
35
35
|
state.pending = other.pending.dup
|
|
36
36
|
state.command = other.command.dup
|
|
37
|
+
state.command_range = other.command_range&.dup
|
|
37
38
|
state.last_change = other.last_change&.dup
|
|
38
39
|
state.last_find = other.last_find&.dup
|
|
39
40
|
state.last_visual_selection = other.last_visual_selection&.dup
|
|
40
|
-
state.visual_block_insert = other.visual_block_insert&.dup
|
|
41
41
|
state.marks = other.marks.transform_values(&:dup)
|
|
42
42
|
state.registers = other.registers.transform_values(&:dup)
|
|
43
43
|
state.register_types = other.register_types.transform_values(&:dup)
|