kward 0.83.0 → 0.85.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 +102 -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 +13 -1
- data/doc/api.md +21 -2
- data/doc/composer.md +2 -2
- data/doc/configuration.md +95 -25
- data/doc/editor.md +28 -13
- data/doc/extensibility.md +2 -1
- data/doc/files.md +8 -4
- data/doc/getting-started.md +3 -0
- data/doc/git.md +3 -1
- data/doc/pan.md +25 -15
- data/doc/permissions.md +4 -4
- data/doc/platform-support.md +48 -0
- data/doc/plugins.md +464 -15
- data/doc/rpc.md +154 -16
- data/doc/sandboxing.md +11 -5
- data/doc/security.md +10 -3
- data/doc/session-management.md +5 -4
- data/doc/shell.md +62 -45
- data/doc/tabs.md +6 -2
- data/doc/transports.md +15 -0
- data/doc/troubleshooting.md +12 -2
- data/doc/usage.md +9 -6
- data/doc/workspace-tools.md +9 -0
- data/examples/plugins/space_invaders.rb +1 -1
- data/examples/plugins/stardate_footer.rb +2 -2
- data/examples/plugins/telegram/plugin.rb +1 -1
- data/kward.gemspec +5 -4
- data/lib/kward/agent.rb +30 -14
- data/lib/kward/cli/auth_commands.rb +34 -13
- data/lib/kward/cli/commands.rb +83 -62
- data/lib/kward/cli/compaction.rb +9 -3
- 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/plugins.rb +81 -12
- data/lib/kward/cli/project_skills_commands.rb +8 -4
- data/lib/kward/cli/prompt_interface.rb +52 -5
- data/lib/kward/cli/rendering.rb +18 -9
- data/lib/kward/cli/runtime_helpers.rb +228 -71
- data/lib/kward/cli/sessions.rb +9 -5
- data/lib/kward/cli/settings/menus.rb +745 -0
- data/lib/kward/cli/settings/model.rb +327 -0
- data/lib/kward/cli/settings.rb +6 -1055
- data/lib/kward/cli/slash_commands.rb +56 -17
- data/lib/kward/cli/tabs.rb +244 -33
- data/lib/kward/cli/tool_summaries.rb +14 -0
- data/lib/kward/{cli_transcript_formatter.rb → cli/transcript_formatter.rb} +14 -7
- data/lib/kward/cli/worktrees.rb +65 -2
- data/lib/kward/cli.rb +70 -30
- data/lib/kward/compactor.rb +18 -7
- data/lib/kward/config/core.rb +389 -0
- data/lib/kward/config/extensions.rb +96 -0
- data/lib/kward/config/prompts.rb +313 -0
- data/lib/kward/config/settings.rb +250 -0
- data/lib/kward/config_files.rb +14 -994
- data/lib/kward/conversation.rb +31 -2
- data/lib/kward/image_attachments.rb +1 -1
- data/lib/kward/model/client.rb +36 -24
- data/lib/kward/model/copilot_models.rb +2 -2
- data/lib/kward/model/model_info.rb +20 -3
- data/lib/kward/{openrouter_model_cache.rb → model/openrouter_model_cache.rb} +3 -3
- data/lib/kward/model/payloads.rb +12 -3
- data/lib/kward/model/provider_catalog.rb +5 -0
- data/lib/kward/model/stream_parser.rb +20 -4
- data/lib/kward/model/typesafe_client.rb +78 -0
- data/lib/kward/pan/index.html.erb +3 -3
- data/lib/kward/pan/server.rb +33 -10
- data/lib/kward/permissions/policy.rb +6 -2
- data/lib/kward/plugin_registry.rb +2 -659
- data/lib/kward/plugins/actions.rb +453 -0
- data/lib/kward/plugins/chat_contract.rb +121 -0
- data/lib/kward/{plugin_chat_runtime.rb → plugins/chat_runtime.rb} +62 -18
- data/lib/kward/plugins/host.rb +232 -0
- data/lib/kward/plugins/registry.rb +1190 -0
- data/lib/kward/plugins/resources.rb +206 -0
- data/lib/kward/plugins/turn_request.rb +36 -0
- data/lib/kward/plugins/ui.rb +219 -0
- 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/markdown_code_block.rb +136 -0
- 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/scratchpad_languages.rb +74 -0
- data/lib/kward/prompt_interface/editor/scratchpad_runner.rb +182 -0
- data/lib/kward/prompt_interface/editor/state.rb +11 -11
- 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/layout.rb +1 -1
- data/lib/kward/prompt_interface/overlay_renderer.rb +7 -5
- data/lib/kward/prompt_interface/plugin_ui_requests.rb +82 -0
- data/lib/kward/prompt_interface/project_browser.rb +415 -14
- data/lib/kward/prompt_interface/runtime_state.rb +56 -2
- data/lib/kward/prompt_interface/screen.rb +11 -4
- 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 +151 -27
- data/lib/kward/prompts/commands.rb +3 -2
- data/lib/kward/prompts.rb +1 -1
- data/lib/kward/{adaptive_pty_output_sink.rb → pty/adaptive_output_sink.rb} +1 -1
- data/lib/kward/pty/detached_run.rb +44 -0
- data/lib/kward/{interactive_pty_runner.rb → pty/interactive_runner.rb} +104 -30
- data/lib/kward/{local_command_runner.rb → pty/local_command_runner.rb} +1 -1
- data/lib/kward/{local_pty_command_runner.rb → pty/local_pty_runner.rb} +3 -9
- data/lib/kward/{pty_output_sink.rb → pty/output_sink.rb} +52 -0
- data/lib/kward/{pty_transcript_normalizer.rb → pty/transcript_normalizer.rb} +1 -1
- data/lib/kward/rpc/plugin_chat_manager.rb +30 -10
- data/lib/kward/rpc/prompt_bridge.rb +25 -0
- data/lib/kward/rpc/server.rb +91 -12
- data/lib/kward/rpc/session_manager.rb +147 -44
- data/lib/kward/rpc/session_tree_rows.rb +2 -2
- data/lib/kward/rpc/tool_metadata.rb +1 -1
- data/lib/kward/rpc/transcript_normalizer.rb +7 -3
- data/lib/kward/sandbox/command_runner.rb +1 -1
- data/lib/kward/{session_catalog.rb → sessions/catalog.rb} +1 -1
- data/lib/kward/{session_store.rb → sessions/store.rb} +9 -9
- data/lib/kward/{session_tree_nodes.rb → sessions/tree_nodes.rb} +3 -3
- data/lib/kward/{session_tree_renderer.rb → sessions/tree_renderer.rb} +4 -4
- data/lib/kward/{session_tree_tool_display.rb → sessions/tree_tool_display.rb} +1 -1
- data/lib/kward/{ekwsh.rb → shell/kwsh.rb} +38 -19
- data/lib/kward/shell/kwshrc.rb +233 -0
- data/lib/kward/{persistent_shell_session.rb → shell/persistent_session.rb} +121 -28
- data/lib/kward/{shell_prompt.rb → shell/prompt.rb} +2 -0
- data/lib/kward/{shell_prompt_session.rb → shell/prompt_session.rb} +1 -1
- data/lib/kward/skills/trust_store.rb +1 -1
- data/lib/kward/tabs/driver.rb +194 -0
- data/lib/kward/{tab_store.rb → tabs/store.rb} +2 -2
- data/lib/kward/{ansi.rb → terminal/ansi.rb} +110 -10
- data/lib/kward/{clipboard.rb → terminal/clipboard.rb} +1 -1
- data/lib/kward/{terminal_image_support.rb → terminal/image_support.rb} +1 -1
- data/lib/kward/{terminal_keys.rb → 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/base.rb +18 -0
- data/lib/kward/tools/context_for_task.rb +15 -6
- data/lib/kward/tools/edit_file.rb +9 -6
- data/lib/kward/tools/git_commit.rb +13 -7
- data/lib/kward/tools/list_directory.rb +4 -4
- data/lib/kward/tools/open_editor.rb +41 -0
- data/lib/kward/tools/plugin_tool.rb +41 -0
- data/lib/kward/tools/prepare_shell_command.rb +1 -1
- data/lib/kward/tools/read_file.rb +7 -6
- data/lib/kward/tools/registry.rb +109 -17
- data/lib/kward/tools/run_shell_command.rb +10 -8
- data/lib/kward/tools/search/code.rb +1 -1
- data/lib/kward/tools/summarize_file_structure.rb +5 -5
- data/lib/kward/tools/tool_call.rb +3 -1
- data/lib/kward/tools/typesafe_evaluate.rb +81 -0
- data/lib/kward/tools/workspace_targets.rb +58 -0
- data/lib/kward/tools/write_file.rb +9 -6
- data/lib/kward/{export_path.rb → transcripts/export_path.rb} +1 -1
- data/lib/kward/{markdown_transcript.rb → transcripts/markdown_transcript.rb} +2 -2
- data/lib/kward/transport/contracts.rb +200 -0
- data/lib/kward/transport/gateway.rb +79 -35
- data/lib/kward/transport/plugin_chat_gateway.rb +3 -2
- data/lib/kward/transport.rb +1 -200
- data/lib/kward/version.rb +1 -1
- data/lib/kward/{workspace_factory.rb → workspace/factory.rb} +2 -2
- data/lib/kward/{project_files.rb → workspace/files.rb} +2 -2
- data/lib/kward/{git_worktree_manager.rb → workspace/git_worktree_manager.rb} +28 -0
- data/lib/kward/{workspace.rb → workspace/workspace.rb} +3 -3
- 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 +2 -2
- data/templates/default/layout/html/setup.rb +41 -2
- metadata +94 -47
- data/lib/kward/scratchpad_runner.rb +0 -56
- data/lib/kward/tab_driver.rb +0 -90
- /data/lib/kward/{editor_prompt.rb → cli/editor_prompt.rb} +0 -0
- /data/lib/kward/{editor_prompt_session.rb → cli/editor_prompt_session.rb} +0 -0
- /data/lib/kward/{diff_view_mode.rb → prompt_interface/editor/diff_view_mode.rb} +0 -0
- /data/lib/kward/{editor_mode.rb → prompt_interface/editor/editor_mode.rb} +0 -0
- /data/lib/kward/{session_diff.rb → sessions/diff.rb} +0 -0
- /data/lib/kward/{session_naming.rb → sessions/naming.rb} +0 -0
- /data/lib/kward/{session_trash.rb → sessions/trash.rb} +0 -0
- /data/lib/kward/{terminal_sequences.rb → terminal/sequences.rb} +0 -0
- /data/lib/kward/{transcript_export.rb → transcripts/transcript_export.rb} +0 -0
- /data/lib/kward/{path_guard.rb → workspace/path_guard.rb} +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
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Namespace for the Kward CLI agent runtime.
|
|
2
|
+
module Kward
|
|
3
|
+
# Names, aliases, and display paths accepted by the scratchpad command.
|
|
4
|
+
module ScratchpadLanguages
|
|
5
|
+
LANGUAGES = {
|
|
6
|
+
text: { aliases: %w[text txt], display_path: "scratchpad.txt" },
|
|
7
|
+
ruby: { aliases: %w[ruby rb], display_path: "scratchpad.rb", runner: :ruby },
|
|
8
|
+
erb: { aliases: %w[erb], display_path: "scratchpad.erb" },
|
|
9
|
+
crystal: { aliases: %w[crystal cr], display_path: "scratchpad.cr", runner: :crystal },
|
|
10
|
+
elixir: { aliases: %w[elixir ex exs], display_path: "scratchpad.ex", runner: :elixir },
|
|
11
|
+
julia: { aliases: %w[julia jl], display_path: "scratchpad.jl", runner: :julia },
|
|
12
|
+
javascript: { aliases: %w[javascript js jsx mjs cjs], display_path: "scratchpad.js", runner: :node },
|
|
13
|
+
typescript: { aliases: %w[typescript ts tsx], display_path: "scratchpad.ts", runner: :node },
|
|
14
|
+
json: { aliases: %w[json], display_path: "scratchpad.json" },
|
|
15
|
+
markdown: { aliases: %w[markdown md], display_path: "scratchpad.md" },
|
|
16
|
+
yaml: { aliases: %w[yaml yml], display_path: "scratchpad.yml" },
|
|
17
|
+
shell: { aliases: %w[shell sh bash zsh fish], display_path: "scratchpad.sh", runner: :shell },
|
|
18
|
+
makefile: { aliases: %w[makefile make mk], display_path: "scratchpad.mk" },
|
|
19
|
+
html: { aliases: %w[html htm], display_path: "scratchpad.html" },
|
|
20
|
+
css: { aliases: %w[css], display_path: "scratchpad.css" },
|
|
21
|
+
scss: { aliases: %w[scss], display_path: "scratchpad.scss" },
|
|
22
|
+
python: { aliases: %w[python py pyw], display_path: "scratchpad.py", runner: :python },
|
|
23
|
+
go: { aliases: %w[go], display_path: "scratchpad.go", runner: :go },
|
|
24
|
+
rust: { aliases: %w[rust rs], display_path: "scratchpad.rs" },
|
|
25
|
+
java: { aliases: %w[java], display_path: "scratchpad.java" },
|
|
26
|
+
csharp: { aliases: %w[csharp cs c#], display_path: "scratchpad.cs" },
|
|
27
|
+
c: { aliases: %w[c], display_path: "scratchpad.c" },
|
|
28
|
+
cpp: { aliases: %w[cpp cc cxx hpp hh hxx c++], display_path: "scratchpad.cpp" },
|
|
29
|
+
swift: { aliases: %w[swift], display_path: "scratchpad.swift", runner: :swift },
|
|
30
|
+
kotlin: { aliases: %w[kotlin kt kts], display_path: "scratchpad.kt" },
|
|
31
|
+
lua: { aliases: %w[lua], display_path: "scratchpad.lua", runner: :lua },
|
|
32
|
+
sql: { aliases: %w[sql], display_path: "scratchpad.sql" }
|
|
33
|
+
}.freeze
|
|
34
|
+
|
|
35
|
+
ALIASES = LANGUAGES.each_with_object({}) do |(language, definition), aliases|
|
|
36
|
+
([language.to_s] + definition.fetch(:aliases)).each do |name|
|
|
37
|
+
aliases[name] = language
|
|
38
|
+
end
|
|
39
|
+
end.freeze
|
|
40
|
+
|
|
41
|
+
module_function
|
|
42
|
+
|
|
43
|
+
def normalize(language)
|
|
44
|
+
value = language.to_s.strip.downcase
|
|
45
|
+
return :text if value.empty?
|
|
46
|
+
|
|
47
|
+
ALIASES.fetch(value) do
|
|
48
|
+
raise ArgumentError, "Unknown scratchpad language #{language.inspect}. Use /scratchpad help for supported languages."
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def display_path(language)
|
|
53
|
+
LANGUAGES.fetch(normalize(language)).fetch(:display_path)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def runner(language)
|
|
57
|
+
LANGUAGES.fetch(normalize(language))[:runner]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def runnable?(language)
|
|
61
|
+
!runner(language).nil?
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def help_text
|
|
65
|
+
lines = ["Supported scratchpad languages:"]
|
|
66
|
+
LANGUAGES.each do |language, definition|
|
|
67
|
+
aliases = definition.fetch(:aliases).reject { |name| name == language.to_s }
|
|
68
|
+
suffix = aliases.empty? ? "" : " (#{aliases.join(", ")})"
|
|
69
|
+
lines << " #{language}#{suffix}"
|
|
70
|
+
end
|
|
71
|
+
lines.join("\n")
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
require "open3"
|
|
2
|
+
require "rbconfig"
|
|
3
|
+
require "tempfile"
|
|
4
|
+
require_relative "scratchpad_languages"
|
|
5
|
+
|
|
6
|
+
# Namespace for the Kward CLI agent runtime.
|
|
7
|
+
module Kward
|
|
8
|
+
# Executes editor buffers and returns captured runner output.
|
|
9
|
+
module ScratchpadRunner
|
|
10
|
+
OUTPUT_READ_SIZE = 16 * 1024
|
|
11
|
+
MAX_OUTPUT_BYTES = 1_000_000
|
|
12
|
+
POLL_INTERVAL = 0.05
|
|
13
|
+
|
|
14
|
+
DEFAULT_BINARIES = {
|
|
15
|
+
ruby: -> { RbConfig.ruby },
|
|
16
|
+
node: "node",
|
|
17
|
+
python: -> { windows_host? ? "python" : "python3" },
|
|
18
|
+
shell: -> { File.executable?("/bin/sh") ? "/bin/sh" : "sh" },
|
|
19
|
+
lua: "lua",
|
|
20
|
+
julia: "julia",
|
|
21
|
+
elixir: "elixir",
|
|
22
|
+
crystal: "crystal",
|
|
23
|
+
swift: "swift",
|
|
24
|
+
go: "go"
|
|
25
|
+
}.freeze
|
|
26
|
+
RUNNER_ARGUMENTS = {
|
|
27
|
+
crystal: ["run"],
|
|
28
|
+
go: ["run"]
|
|
29
|
+
}.freeze
|
|
30
|
+
|
|
31
|
+
Result = Struct.new(:language, :output, :exit_status, :duration, :command, :cancelled, :truncated, keyword_init: true)
|
|
32
|
+
|
|
33
|
+
module_function
|
|
34
|
+
|
|
35
|
+
def run(language, content, cancelled: nil, cwd: Dir.pwd, source_path: nil, runner_config: {})
|
|
36
|
+
language = ScratchpadLanguages.normalize(language)
|
|
37
|
+
runner = ScratchpadLanguages.runner(language)
|
|
38
|
+
raise ArgumentError, "Scratchpad language #{language.inspect} is not runnable" unless runner
|
|
39
|
+
|
|
40
|
+
cwd = File.expand_path(cwd.to_s.empty? ? Dir.pwd : cwd)
|
|
41
|
+
binary = configured_binary(runner, runner_config, cwd)
|
|
42
|
+
arguments = RUNNER_ARGUMENTS.fetch(runner, [])
|
|
43
|
+
default_display_path = ScratchpadLanguages.display_path(language)
|
|
44
|
+
display_path = source_path.to_s.empty? ? default_display_path : File.basename(source_path.to_s)
|
|
45
|
+
extension = File.extname(display_path)
|
|
46
|
+
extension = File.extname(default_display_path) if extension.empty?
|
|
47
|
+
started_at = monotonic_now
|
|
48
|
+
output, status, was_cancelled, truncated = capture_source(
|
|
49
|
+
content,
|
|
50
|
+
extension: extension,
|
|
51
|
+
source_path: source_path,
|
|
52
|
+
cwd: cwd,
|
|
53
|
+
command: [binary, *arguments],
|
|
54
|
+
cancelled: cancelled
|
|
55
|
+
)
|
|
56
|
+
Result.new(
|
|
57
|
+
language: language,
|
|
58
|
+
output: output,
|
|
59
|
+
exit_status: was_cancelled ? 130 : status.exitstatus,
|
|
60
|
+
duration: monotonic_now - started_at,
|
|
61
|
+
command: [binary, *arguments, "<#{display_path}>"],
|
|
62
|
+
cancelled: was_cancelled,
|
|
63
|
+
truncated: truncated
|
|
64
|
+
)
|
|
65
|
+
rescue Errno::ENOENT
|
|
66
|
+
raise ArgumentError, "Editor runner #{runner.inspect} binary #{binary.inspect} was not found"
|
|
67
|
+
rescue Errno::EACCES
|
|
68
|
+
raise ArgumentError, "Editor runner #{runner.inspect} binary #{binary.inspect} is not executable"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def capture_source(content, extension:, source_path:, cwd:, command:, cancelled: nil)
|
|
72
|
+
Tempfile.create(["kward-scratchpad", extension], dir: temporary_directory(cwd, source_path)) do |file|
|
|
73
|
+
file.write(content.to_s)
|
|
74
|
+
file.flush
|
|
75
|
+
capture_process([*command, file.path], cancelled: cancelled, chdir: cwd)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def configured_binary(runner, runner_config, cwd)
|
|
80
|
+
settings = runner_config.is_a?(Hash) ? (runner_config[runner.to_s] || runner_config[runner]) : nil
|
|
81
|
+
configured = settings.is_a?(Hash) ? (settings["binary"] || settings[:binary]) : settings
|
|
82
|
+
binary = configured.nil? ? default_binary(runner) : configured
|
|
83
|
+
unless binary.is_a?(String) && !binary.strip.empty?
|
|
84
|
+
raise ArgumentError, "Editor runner #{runner.inspect} binary must be a non-empty string"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
binary = binary.strip
|
|
88
|
+
path_like?(binary) ? File.expand_path(binary, cwd) : binary
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def default_binary(runner)
|
|
92
|
+
value = DEFAULT_BINARIES[runner]
|
|
93
|
+
value.respond_to?(:call) ? value.call : value
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def temporary_directory(cwd, source_path)
|
|
97
|
+
return cwd if source_path.to_s.empty?
|
|
98
|
+
|
|
99
|
+
directory = File.dirname(File.expand_path(source_path, cwd))
|
|
100
|
+
return directory if Dir.exist?(directory) && File.writable?(directory)
|
|
101
|
+
|
|
102
|
+
cwd
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def path_like?(value)
|
|
106
|
+
value.include?("/") || value.include?("\\") || value.start_with?("~")
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def windows_host?
|
|
110
|
+
RbConfig::CONFIG["host_os"].to_s.match?(/mswin|mingw|cygwin/i)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def capture_process(command, cancelled: nil, chdir: nil)
|
|
114
|
+
options = chdir ? { chdir: chdir } : {}
|
|
115
|
+
input, output, wait_thread = Open3.popen2e(*command, **options)
|
|
116
|
+
input.close
|
|
117
|
+
chunks = []
|
|
118
|
+
output_size = 0
|
|
119
|
+
was_cancelled = false
|
|
120
|
+
|
|
121
|
+
loop do
|
|
122
|
+
if !was_cancelled && cancelled&.call
|
|
123
|
+
terminate_process(wait_thread.pid)
|
|
124
|
+
was_cancelled = true
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
output_size = read_available_output(output, chunks, output_size)
|
|
128
|
+
break if wait_thread.join(0)
|
|
129
|
+
|
|
130
|
+
sleep POLL_INTERVAL
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
output_size = read_remaining_output(output, chunks, output_size)
|
|
134
|
+
[chunks.join, wait_thread.value, was_cancelled, output_size >= MAX_OUTPUT_BYTES]
|
|
135
|
+
ensure
|
|
136
|
+
if wait_thread&.alive?
|
|
137
|
+
terminate_process(wait_thread.pid)
|
|
138
|
+
wait_thread.join
|
|
139
|
+
end
|
|
140
|
+
input&.close unless input&.closed?
|
|
141
|
+
output&.close unless output&.closed?
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def read_available_output(output, chunks, output_size)
|
|
145
|
+
ready = IO.select([output], nil, nil, 0)
|
|
146
|
+
return output_size unless ready
|
|
147
|
+
|
|
148
|
+
loop do
|
|
149
|
+
output_size = append_output(chunks, output.read_nonblock(OUTPUT_READ_SIZE), output_size)
|
|
150
|
+
rescue IO::WaitReadable, EOFError
|
|
151
|
+
break
|
|
152
|
+
end
|
|
153
|
+
output_size
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def read_remaining_output(output, chunks, output_size)
|
|
157
|
+
loop do
|
|
158
|
+
output_size = append_output(chunks, output.read_nonblock(OUTPUT_READ_SIZE), output_size)
|
|
159
|
+
end
|
|
160
|
+
rescue IO::WaitReadable, IOError, EOFError
|
|
161
|
+
output_size
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def append_output(chunks, chunk, output_size)
|
|
165
|
+
return output_size if output_size >= MAX_OUTPUT_BYTES
|
|
166
|
+
|
|
167
|
+
remaining = MAX_OUTPUT_BYTES - output_size
|
|
168
|
+
chunks << chunk.byteslice(0, remaining)
|
|
169
|
+
output_size + [chunk.bytesize, remaining].min
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def terminate_process(pid)
|
|
173
|
+
Process.kill("TERM", pid)
|
|
174
|
+
rescue Errno::ESRCH
|
|
175
|
+
nil
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def monotonic_now
|
|
179
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|