kward 0.80.0 → 0.81.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 +40 -0
- data/Gemfile.lock +2 -2
- data/README.md +1 -1
- data/doc/composer.md +5 -8
- data/doc/configuration.md +13 -3
- data/doc/permissions.md +1 -1
- data/doc/rpc.md +2 -1
- data/doc/sandboxing.md +4 -4
- data/doc/security.md +6 -9
- data/doc/shell.md +161 -196
- data/doc/skills.md +19 -5
- data/doc/tabs.md +1 -1
- data/doc/usage.md +6 -5
- data/lib/kward/agent.rb +11 -6
- data/lib/kward/ansi.rb +9 -1
- data/lib/kward/cli/commands.rb +7 -0
- data/lib/kward/cli/interactive_turn.rb +1 -1
- data/lib/kward/cli/memory_commands.rb +2 -2
- data/lib/kward/cli/plugins.rb +1 -1
- data/lib/kward/cli/project_skills.rb +99 -0
- data/lib/kward/cli/project_skills_commands.rb +87 -0
- data/lib/kward/cli/prompt_interface.rb +54 -4
- data/lib/kward/cli/rendering.rb +28 -2
- data/lib/kward/cli/runtime_helpers.rb +157 -19
- data/lib/kward/cli/sessions.rb +6 -4
- data/lib/kward/cli/settings.rb +1 -1
- data/lib/kward/cli/slash_commands.rb +7 -1
- data/lib/kward/cli/tabs.rb +4 -1
- data/lib/kward/cli.rb +21 -1
- data/lib/kward/compactor.rb +7 -2
- data/lib/kward/config_files.rb +30 -9
- data/lib/kward/conversation.rb +8 -5
- data/lib/kward/ekwsh.rb +37 -16
- data/lib/kward/hooks/audit_log.rb +5 -2
- data/lib/kward/interactive_pty_runner.rb +88 -22
- data/lib/kward/plugin_registry.rb +20 -13
- data/lib/kward/prompt_interface/composer_controller.rb +13 -1
- data/lib/kward/prompt_interface/editor/controller.rb +4 -0
- data/lib/kward/prompt_interface/editor/syntax_highlighter.rb +35 -10
- data/lib/kward/prompt_interface/key_handler.rb +78 -43
- data/lib/kward/prompt_interface/overlay_renderer.rb +12 -0
- data/lib/kward/prompt_interface/screen.rb +5 -0
- data/lib/kward/prompt_interface.rb +93 -21
- data/lib/kward/prompts/commands.rb +2 -0
- data/lib/kward/prompts/templates.rb +11 -6
- data/lib/kward/prompts.rb +6 -6
- data/lib/kward/rpc/server.rb +1 -0
- data/lib/kward/session_store.rb +3 -2
- data/lib/kward/skills/registry.rb +67 -12
- data/lib/kward/skills/trust_coordinator.rb +45 -0
- data/lib/kward/skills/trust_store.rb +107 -0
- data/lib/kward/telemetry/logger.rb +5 -2
- data/lib/kward/version.rb +1 -1
- metadata +5 -1
data/lib/kward/ekwsh.rb
CHANGED
|
@@ -13,7 +13,7 @@ module Kward
|
|
|
13
13
|
class Ekwsh
|
|
14
14
|
Result = Struct.new(:output, :exit_status, :exit_shell, :clear, :open_editor_path, :interactive_command, :streamed, keyword_init: true)
|
|
15
15
|
Completion = Struct.new(:range, :replacement, :candidates, keyword_init: true)
|
|
16
|
-
BUILTINS = %w[alias cd pwd export unset unalias clear exit logout pty].freeze
|
|
16
|
+
BUILTINS = %w[alias capture cd pwd export unset unalias clear exit logout pty].freeze
|
|
17
17
|
DEFAULT_SHELL = "/bin/sh"
|
|
18
18
|
DEFAULT_TIMEOUT_SECONDS = 300
|
|
19
19
|
DEFAULT_MAX_OUTPUT_BYTES = 1_048_576
|
|
@@ -72,6 +72,23 @@ module Kward
|
|
|
72
72
|
Completion.new(range: token[:range], replacement: replacement, candidates: candidates)
|
|
73
73
|
end
|
|
74
74
|
|
|
75
|
+
def expand_alias(command, interactive: false)
|
|
76
|
+
words = shell_words(command)
|
|
77
|
+
return command if words.empty? || BUILTINS.include?(words.first)
|
|
78
|
+
return command unless @aliases[words.first]
|
|
79
|
+
|
|
80
|
+
rest = command.sub(/\A\s*#{Regexp.escape(words.first)}\b\s*/, "")
|
|
81
|
+
expanded = [@aliases.fetch(words.first), rest].reject(&:empty?).join(" ")
|
|
82
|
+
interactive ? expanded.sub(/\A\s*(?:capture|pty)(?:\s+|\z)/, "") : expanded
|
|
83
|
+
rescue ArgumentError
|
|
84
|
+
command
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def editor_command_result(command, display_command: command)
|
|
88
|
+
expanded_command = expand_alias(command, interactive: true)
|
|
89
|
+
kward_command_result(expanded_command, display_command: display_command)
|
|
90
|
+
end
|
|
91
|
+
|
|
75
92
|
private
|
|
76
93
|
|
|
77
94
|
def configure_rbenv_environment
|
|
@@ -276,7 +293,7 @@ module Kward
|
|
|
276
293
|
Result.new(output: "#{command_echo(display_command)}ekwsh: #{e.message}\n", exit_status: 2)
|
|
277
294
|
end
|
|
278
295
|
|
|
279
|
-
def builtin_result(command, display_command: command)
|
|
296
|
+
def builtin_result(command, display_command: command, cancellation: nil, &block)
|
|
280
297
|
words = shell_words(command)
|
|
281
298
|
return nil if words.empty?
|
|
282
299
|
assignment_result = persist_assignments(display_command, words)
|
|
@@ -297,6 +314,8 @@ module Kward
|
|
|
297
314
|
unset_variables(display_command, words)
|
|
298
315
|
when "clear"
|
|
299
316
|
Result.new(output: "", exit_status: 0, clear: true)
|
|
317
|
+
when "capture"
|
|
318
|
+
captured_command_result(command, display_command: display_command, cancellation: cancellation, &block)
|
|
300
319
|
when "pty"
|
|
301
320
|
interactive_pty_result(command, display_command: display_command)
|
|
302
321
|
else
|
|
@@ -306,13 +325,26 @@ module Kward
|
|
|
306
325
|
Result.new(output: "#{command_echo(display_command)}ekwsh: #{e.message}\n", exit_status: 2)
|
|
307
326
|
end
|
|
308
327
|
|
|
328
|
+
def captured_command_result(command, display_command:, cancellation: nil, &block)
|
|
329
|
+
captured_command = command.sub(/\A\s*capture(?:\s+|\z)/, "")
|
|
330
|
+
if captured_command.empty?
|
|
331
|
+
return Result.new(output: "#{command_echo(display_command)}Usage: capture <command>\n", exit_status: 2)
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
execute(captured_command, display_command: display_command, cancellation: cancellation, &block)
|
|
335
|
+
end
|
|
336
|
+
|
|
309
337
|
def interactive_pty_result(command, display_command: command)
|
|
310
338
|
interactive_command = command.sub(/\A\s*pty(?:\s+|\z)/, "")
|
|
311
339
|
if interactive_command.empty?
|
|
312
340
|
return Result.new(output: "#{command_echo(display_command)}Usage: pty <command>\n", exit_status: 2)
|
|
313
341
|
end
|
|
314
342
|
|
|
315
|
-
|
|
343
|
+
interactive_command_result(interactive_command, display_command: display_command)
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def interactive_command_result(command, display_command: command)
|
|
347
|
+
Result.new(output: command_echo(display_command), exit_status: 0, interactive_command: command)
|
|
316
348
|
end
|
|
317
349
|
|
|
318
350
|
def shell_words(command)
|
|
@@ -361,29 +393,18 @@ module Kward
|
|
|
361
393
|
name.to_s.match?(/\A[A-Za-z_][A-Za-z0-9_-]*\z/) && !BUILTINS.include?(name.to_s)
|
|
362
394
|
end
|
|
363
395
|
|
|
364
|
-
def expand_alias(command)
|
|
365
|
-
words = shell_words(command)
|
|
366
|
-
return command if words.empty? || BUILTINS.include?(words.first)
|
|
367
|
-
return command unless @aliases[words.first]
|
|
368
|
-
|
|
369
|
-
rest = command.sub(/\A\s*#{Regexp.escape(words.first)}\b\s*/, "")
|
|
370
|
-
[@aliases.fetch(words.first), rest].reject(&:empty?).join(" ")
|
|
371
|
-
rescue ArgumentError
|
|
372
|
-
command
|
|
373
|
-
end
|
|
374
|
-
|
|
375
396
|
def run_expanded_command(command, cancellation: nil, &block)
|
|
376
397
|
expanded_command = expand_alias(command)
|
|
377
398
|
exit_result = exit_result(expanded_command, display_command: command)
|
|
378
399
|
return exit_result if exit_result
|
|
379
400
|
|
|
380
|
-
builtin_result = builtin_result(expanded_command, display_command: command)
|
|
401
|
+
builtin_result = builtin_result(expanded_command, display_command: command, cancellation: cancellation, &block)
|
|
381
402
|
return builtin_result if builtin_result
|
|
382
403
|
|
|
383
404
|
kward_result = kward_command_result(expanded_command, display_command: command)
|
|
384
405
|
return kward_result if kward_result
|
|
385
406
|
|
|
386
|
-
|
|
407
|
+
interactive_command_result(expanded_command, display_command: command)
|
|
387
408
|
end
|
|
388
409
|
|
|
389
410
|
def kward_command_result(command, display_command: command)
|
|
@@ -11,13 +11,14 @@ module Kward
|
|
|
11
11
|
class AuditLog
|
|
12
12
|
DEFAULT_MAX_BYTES = 10 * 1024 * 1024
|
|
13
13
|
|
|
14
|
-
def initialize(path: nil, config_path: ConfigFiles.config_path, max_bytes: DEFAULT_MAX_BYTES, clock: Time, monotonic_clock: Process, error_output: $stderr)
|
|
14
|
+
def initialize(path: nil, config_path: ConfigFiles.config_path, max_bytes: DEFAULT_MAX_BYTES, clock: Time, monotonic_clock: Process, error_output: $stderr, warning_sink: nil)
|
|
15
15
|
@path = path
|
|
16
16
|
@config_path = config_path
|
|
17
17
|
@max_bytes = max_bytes.to_i.positive? ? max_bytes.to_i : DEFAULT_MAX_BYTES
|
|
18
18
|
@clock = clock
|
|
19
19
|
@monotonic_clock = monotonic_clock
|
|
20
20
|
@error_output = error_output
|
|
21
|
+
@warning_sink = warning_sink
|
|
21
22
|
@mutex = Mutex.new
|
|
22
23
|
@warned = false
|
|
23
24
|
end
|
|
@@ -112,7 +113,9 @@ module Kward
|
|
|
112
113
|
return if @warned
|
|
113
114
|
|
|
114
115
|
@warned = true
|
|
115
|
-
|
|
116
|
+
message = "Warning: hook audit logging failed: #{error.message}"
|
|
117
|
+
sink = @warning_sink || ConfigFiles.warning_sink
|
|
118
|
+
sink ? sink.call(message) : @error_output&.puts(message)
|
|
116
119
|
rescue StandardError
|
|
117
120
|
nil
|
|
118
121
|
end
|
|
@@ -9,7 +9,7 @@ module Kward
|
|
|
9
9
|
# This is intentionally low level: UI orchestration decides when terminal
|
|
10
10
|
# ownership is handed to the child process and how the result is presented.
|
|
11
11
|
class InteractivePtyRunner
|
|
12
|
-
Result = Struct.new(:exit_status, keyword_init: true)
|
|
12
|
+
Result = Struct.new(:exit_status, :input_forwarded, keyword_init: true)
|
|
13
13
|
|
|
14
14
|
READ_SIZE = 4096
|
|
15
15
|
|
|
@@ -18,34 +18,27 @@ module Kward
|
|
|
18
18
|
@window_size = nil
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
-
def run(*command, env: {}, cwd: Dir.pwd, input: $stdin, output: $stdout)
|
|
21
|
+
def run(*command, env: {}, cwd: Dir.pwd, input: $stdin, output: $stdout, &block)
|
|
22
22
|
pid = nil
|
|
23
23
|
status = nil
|
|
24
|
+
writer = nil
|
|
25
|
+
input_forwarded = false
|
|
24
26
|
|
|
25
|
-
PTY.spawn(env.to_h, *command, chdir: cwd.to_s) do |reader,
|
|
27
|
+
PTY.spawn(env.to_h, *command, chdir: cwd.to_s) do |reader, child_writer, child_pid|
|
|
26
28
|
pid = child_pid
|
|
29
|
+
writer = child_writer
|
|
27
30
|
update_window_size(reader, pid)
|
|
28
31
|
with_raw_input(input) do
|
|
29
32
|
drain_initial_input(input, writer)
|
|
30
|
-
|
|
31
|
-
update_window_size(reader, pid)
|
|
32
|
-
readable = IO.select([reader, input], nil, nil, 0.02)&.first || []
|
|
33
|
-
forward_pty_output(reader, output) if readable.include?(reader)
|
|
34
|
-
forward_input(input, writer) if readable.include?(input)
|
|
35
|
-
if (finished_status = finished_status(pid))
|
|
36
|
-
status = finished_status
|
|
37
|
-
break
|
|
38
|
-
end
|
|
39
|
-
rescue Errno::EIO, IOError
|
|
40
|
-
break
|
|
41
|
-
end
|
|
33
|
+
status, input_forwarded = forward_io(reader, writer, pid, input, output, &block)
|
|
42
34
|
end
|
|
43
35
|
status ||= wait_for_status(pid)
|
|
44
|
-
ensure
|
|
45
|
-
writer&.close unless writer&.closed?
|
|
46
36
|
end
|
|
47
37
|
|
|
48
|
-
Result.new(exit_status: exit_status(status))
|
|
38
|
+
Result.new(exit_status: exit_status(status), input_forwarded: input_forwarded)
|
|
39
|
+
ensure
|
|
40
|
+
writer&.close unless writer&.closed?
|
|
41
|
+
terminate_and_reap(pid) if pid && status.nil?
|
|
49
42
|
end
|
|
50
43
|
|
|
51
44
|
private
|
|
@@ -53,8 +46,14 @@ module Kward
|
|
|
53
46
|
def with_raw_input(input)
|
|
54
47
|
return yield unless input.respond_to?(:raw)
|
|
55
48
|
|
|
56
|
-
|
|
49
|
+
yielded = false
|
|
50
|
+
input.raw do
|
|
51
|
+
yielded = true
|
|
52
|
+
yield
|
|
53
|
+
end
|
|
57
54
|
rescue Errno::ENOTTY
|
|
55
|
+
raise if yielded
|
|
56
|
+
|
|
58
57
|
yield
|
|
59
58
|
end
|
|
60
59
|
|
|
@@ -70,22 +69,67 @@ module Kward
|
|
|
70
69
|
nil
|
|
71
70
|
end
|
|
72
71
|
|
|
72
|
+
def forward_io(reader, writer, pid, input, output, &block)
|
|
73
|
+
input_open = true
|
|
74
|
+
input_forwarded = false
|
|
75
|
+
loop do
|
|
76
|
+
update_window_size(reader, pid)
|
|
77
|
+
readers = [reader]
|
|
78
|
+
readers << input if input_open
|
|
79
|
+
readable = IO.select(readers, nil, nil, 0.02)&.first || []
|
|
80
|
+
forward_pty_output(reader, output, &block) if readable.include?(reader)
|
|
81
|
+
if input_open && readable.include?(input)
|
|
82
|
+
input_open, forwarded = forward_input(input, writer)
|
|
83
|
+
input_forwarded ||= forwarded
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
status = finished_status(pid)
|
|
87
|
+
next unless status
|
|
88
|
+
|
|
89
|
+
return [finish_stopped_process(pid), input_forwarded] if status.stopped?
|
|
90
|
+
|
|
91
|
+
drain_pty_output(reader, output, &block)
|
|
92
|
+
return [status, input_forwarded]
|
|
93
|
+
rescue Errno::EIO
|
|
94
|
+
return [wait_for_status(pid), input_forwarded]
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
73
98
|
def forward_pty_output(reader, output)
|
|
74
99
|
chunk = reader.read_nonblock(READ_SIZE, exception: false)
|
|
75
100
|
return if chunk.nil? || chunk == :wait_readable
|
|
76
101
|
|
|
77
102
|
output.write(chunk)
|
|
78
103
|
output.flush if output.respond_to?(:flush)
|
|
104
|
+
yield chunk if block_given?
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def drain_pty_output(reader, output)
|
|
108
|
+
loop do
|
|
109
|
+
readable, = IO.select([reader], nil, nil, 0.02)
|
|
110
|
+
break unless readable
|
|
111
|
+
|
|
112
|
+
chunk = reader.read_nonblock(READ_SIZE, exception: false)
|
|
113
|
+
break if chunk.nil? || chunk == :wait_readable
|
|
114
|
+
|
|
115
|
+
output.write(chunk)
|
|
116
|
+
yield chunk if block_given?
|
|
117
|
+
end
|
|
118
|
+
output.flush if output.respond_to?(:flush)
|
|
119
|
+
rescue Errno::EIO
|
|
120
|
+
nil
|
|
79
121
|
end
|
|
80
122
|
|
|
81
123
|
def forward_input(input, writer)
|
|
82
124
|
chunk = input.read_nonblock(READ_SIZE, exception: false)
|
|
83
|
-
return if chunk.nil?
|
|
125
|
+
return [false, false] if chunk.nil?
|
|
126
|
+
return [true, false] if chunk == :wait_readable
|
|
84
127
|
|
|
85
128
|
writer.write(chunk)
|
|
86
129
|
writer.flush
|
|
130
|
+
[true, true]
|
|
87
131
|
rescue Errno::EIO, Errno::EPIPE, IOError
|
|
88
|
-
|
|
132
|
+
[false, false]
|
|
89
133
|
end
|
|
90
134
|
|
|
91
135
|
def update_window_size(reader, pid)
|
|
@@ -111,12 +155,18 @@ module Kward
|
|
|
111
155
|
def finished_status(pid)
|
|
112
156
|
return unless pid
|
|
113
157
|
|
|
114
|
-
finished_pid, status = Process.wait2(pid, Process::WNOHANG)
|
|
158
|
+
finished_pid, status = Process.wait2(pid, Process::WNOHANG | Process::WUNTRACED)
|
|
115
159
|
status if finished_pid
|
|
116
160
|
rescue Errno::ECHILD
|
|
117
161
|
nil
|
|
118
162
|
end
|
|
119
163
|
|
|
164
|
+
def finish_stopped_process(pid)
|
|
165
|
+
signal_process("CONT", -pid) || signal_process("CONT", pid)
|
|
166
|
+
terminate_process_group(pid)
|
|
167
|
+
wait_for_status(pid)
|
|
168
|
+
end
|
|
169
|
+
|
|
120
170
|
def wait_for_status(pid)
|
|
121
171
|
return unless pid
|
|
122
172
|
|
|
@@ -134,6 +184,22 @@ module Kward
|
|
|
134
184
|
1
|
|
135
185
|
end
|
|
136
186
|
|
|
187
|
+
def terminate_and_reap(pid)
|
|
188
|
+
terminate_process_group(pid)
|
|
189
|
+
wait_for_status(pid)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def terminate_process_group(pid)
|
|
193
|
+
signal_process("TERM", -pid) || signal_process("TERM", pid)
|
|
194
|
+
deadline = Time.now + 0.2
|
|
195
|
+
while Time.now < deadline
|
|
196
|
+
return unless process_running?(pid)
|
|
197
|
+
|
|
198
|
+
sleep 0.02
|
|
199
|
+
end
|
|
200
|
+
signal_process("KILL", -pid) || signal_process("KILL", pid)
|
|
201
|
+
end
|
|
202
|
+
|
|
137
203
|
def process_running?(pid)
|
|
138
204
|
Process.kill(0, pid)
|
|
139
205
|
true
|
|
@@ -313,16 +313,19 @@ module Kward
|
|
|
313
313
|
class << self
|
|
314
314
|
attr_accessor :loading_registry, :loading_path
|
|
315
315
|
|
|
316
|
-
def load(paths:
|
|
317
|
-
|
|
316
|
+
def load(paths: nil, reserved_commands: [], warning_sink: nil)
|
|
317
|
+
warning_sink ||= ConfigFiles.warning_sink
|
|
318
|
+
paths ||= ConfigFiles.plugin_paths(warning_sink: warning_sink)
|
|
319
|
+
registry = new(reserved_commands: reserved_commands, warning_sink: warning_sink)
|
|
318
320
|
paths.each { |path| registry.load_file(path) }
|
|
319
321
|
registry
|
|
320
322
|
end
|
|
321
323
|
end
|
|
322
324
|
|
|
323
325
|
# Creates an object for trusted plugin loading and dispatch.
|
|
324
|
-
def initialize(reserved_commands: [])
|
|
326
|
+
def initialize(reserved_commands: [], warning_sink: nil)
|
|
325
327
|
@reserved_commands = reserved_commands.map(&:to_s)
|
|
328
|
+
@warning_sink = warning_sink
|
|
326
329
|
@commands = {}
|
|
327
330
|
@interactive_commands = {}
|
|
328
331
|
@tab_types = {}
|
|
@@ -419,7 +422,7 @@ module Kward
|
|
|
419
422
|
rendered = entry[:renderer].call(context)
|
|
420
423
|
parts << rendered.to_s unless rendered.to_s.empty?
|
|
421
424
|
rescue StandardError => e
|
|
422
|
-
|
|
425
|
+
emit_warning "Warning: Kward plugin prompt context error in #{entry[:path]}: #{e.message}"
|
|
423
426
|
end
|
|
424
427
|
parts.empty? ? nil : parts.join("\n\n")
|
|
425
428
|
end
|
|
@@ -431,7 +434,7 @@ module Kward
|
|
|
431
434
|
@transcript_event_handlers.each do |entry|
|
|
432
435
|
entry[:handler].call(transcript_event, context)
|
|
433
436
|
rescue StandardError => e
|
|
434
|
-
|
|
437
|
+
emit_warning "Warning: Kward plugin transcript event error in #{entry[:path]}: #{e.message}"
|
|
435
438
|
end
|
|
436
439
|
nil
|
|
437
440
|
end
|
|
@@ -444,7 +447,7 @@ module Kward
|
|
|
444
447
|
Kernel.load(path, true)
|
|
445
448
|
@paths << path
|
|
446
449
|
rescue StandardError => e
|
|
447
|
-
|
|
450
|
+
emit_warning "Warning: skipping Kward plugin #{path}: #{e.message}"
|
|
448
451
|
ensure
|
|
449
452
|
self.class.loading_registry = previous_registry
|
|
450
453
|
self.class.loading_path = previous_path
|
|
@@ -462,11 +465,11 @@ module Kward
|
|
|
462
465
|
raise "Plugin command /#{name} requires a handler" unless handler
|
|
463
466
|
|
|
464
467
|
if @reserved_commands.include?(name)
|
|
465
|
-
|
|
468
|
+
emit_warning "Warning: skipping Kward plugin command /#{name}: reserved command"
|
|
466
469
|
return nil
|
|
467
470
|
end
|
|
468
471
|
if @commands.key?(name)
|
|
469
|
-
|
|
472
|
+
emit_warning "Warning: skipping duplicate Kward plugin command /#{name}: #{path}"
|
|
470
473
|
return nil
|
|
471
474
|
end
|
|
472
475
|
|
|
@@ -485,11 +488,11 @@ module Kward
|
|
|
485
488
|
raise "Interactive command /#{name} requires a handler" unless handler
|
|
486
489
|
|
|
487
490
|
if @reserved_commands.include?(name) || @commands.key?(name)
|
|
488
|
-
|
|
491
|
+
emit_warning "Warning: skipping Kward interactive command /#{name}: reserved command"
|
|
489
492
|
return nil
|
|
490
493
|
end
|
|
491
494
|
if @interactive_commands.key?(name)
|
|
492
|
-
|
|
495
|
+
emit_warning "Warning: skipping duplicate Kward interactive command /#{name}: #{path}"
|
|
493
496
|
return nil
|
|
494
497
|
end
|
|
495
498
|
|
|
@@ -512,7 +515,7 @@ module Kward
|
|
|
512
515
|
raise "Plugin tab type #{name} requires a handler" unless handler
|
|
513
516
|
|
|
514
517
|
if @tab_types.key?(name) || @tab_types_by_id.key?(id)
|
|
515
|
-
|
|
518
|
+
emit_warning "Warning: skipping duplicate Kward plugin tab type #{id}: #{path}"
|
|
516
519
|
return nil
|
|
517
520
|
end
|
|
518
521
|
|
|
@@ -529,7 +532,7 @@ module Kward
|
|
|
529
532
|
raise "Plugin transport #{name} requires a handler" unless handler
|
|
530
533
|
|
|
531
534
|
if @transports.key?(name) || @transports_by_id.key?(id)
|
|
532
|
-
|
|
535
|
+
emit_warning "Warning: skipping duplicate Kward plugin transport #{id}: #{path}"
|
|
533
536
|
return nil
|
|
534
537
|
end
|
|
535
538
|
|
|
@@ -543,11 +546,15 @@ module Kward
|
|
|
543
546
|
def register_footer(path: nil, &renderer)
|
|
544
547
|
raise "Plugin footer requires a renderer" unless renderer
|
|
545
548
|
|
|
546
|
-
|
|
549
|
+
emit_warning "Warning: replacing Kward plugin footer from #{@footer_path}: #{path}" if @footer
|
|
547
550
|
@footer = renderer
|
|
548
551
|
@footer_path = path
|
|
549
552
|
end
|
|
550
553
|
|
|
554
|
+
def emit_warning(message)
|
|
555
|
+
@warning_sink ? @warning_sink.call(message) : warn(message)
|
|
556
|
+
end
|
|
557
|
+
|
|
551
558
|
def register_transcript_event(path: nil, &handler)
|
|
552
559
|
raise "Plugin transcript event requires a handler" unless handler
|
|
553
560
|
|
|
@@ -256,11 +256,23 @@ module Kward
|
|
|
256
256
|
def submit_input
|
|
257
257
|
value = submitted_input
|
|
258
258
|
add_history(composer_input)
|
|
259
|
-
|
|
259
|
+
if !@busy && value.strip.empty?
|
|
260
|
+
@output_io.flush
|
|
261
|
+
return value
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
clear_submitted_input_locked
|
|
260
265
|
@output_io.flush
|
|
261
266
|
value
|
|
262
267
|
end
|
|
263
268
|
|
|
269
|
+
def clear_submitted_input_locked
|
|
270
|
+
self.composer_input = ""
|
|
271
|
+
self.composer_cursor = 0
|
|
272
|
+
@composer.clear_attachments
|
|
273
|
+
reset_history_search
|
|
274
|
+
end
|
|
275
|
+
|
|
264
276
|
def clear_finished_input_locked(reset_history: false)
|
|
265
277
|
if @busy
|
|
266
278
|
clear_prompt_for_output_locked
|
|
@@ -827,6 +827,10 @@ module Kward
|
|
|
827
827
|
modifier = sequence[:modifier]
|
|
828
828
|
queue_pending_keys(sequence[:remaining]) if sequence[:remaining] && !sequence[:remaining].empty?
|
|
829
829
|
|
|
830
|
+
if ctrl_modifier?(modifier) && ctrl_code(code) == 113
|
|
831
|
+
return close_editor
|
|
832
|
+
end
|
|
833
|
+
|
|
830
834
|
if ctrl_modifier?(modifier) && ctrl_code(code) == 102
|
|
831
835
|
return editor_search_active? ? editor_search_append(key) : editor_search_begin
|
|
832
836
|
end
|
|
@@ -23,10 +23,12 @@ module Kward
|
|
|
23
23
|
LANGUAGE_DEFINITIONS = {
|
|
24
24
|
javascript: {
|
|
25
25
|
extensions: %w[.js .jsx .mjs .cjs],
|
|
26
|
+
block_comment: true,
|
|
26
27
|
keywords: %w[async await break case catch class const continue debugger default delete do else export extends false finally for from function if import in instanceof let new null of return static super switch this throw true try typeof undefined var void while with yield]
|
|
27
28
|
},
|
|
28
29
|
typescript: {
|
|
29
30
|
extensions: %w[.ts .tsx],
|
|
31
|
+
block_comment: true,
|
|
30
32
|
keywords: %w[abstract any as async await boolean break case catch class const constructor continue debugger declare default delete do else enum export extends false finally for from function if implements import in infer instanceof interface is keyof let module namespace never new null number object of private protected public readonly return static string super switch symbol this throw true try type typeof undefined unknown var void while with yield]
|
|
31
33
|
},
|
|
32
34
|
shell: {
|
|
@@ -62,34 +64,42 @@ module Kward
|
|
|
62
64
|
},
|
|
63
65
|
go: {
|
|
64
66
|
extensions: %w[.go],
|
|
67
|
+
block_comment: true,
|
|
65
68
|
keywords: %w[break case chan const continue default defer else fallthrough false for func go goto if import interface map nil package range return select struct switch true type var]
|
|
66
69
|
},
|
|
67
70
|
rust: {
|
|
68
71
|
extensions: %w[.rs],
|
|
72
|
+
block_comment: true,
|
|
69
73
|
keywords: %w[as async await break const continue crate dyn else enum extern false fn for if impl in let loop match mod move mut pub ref return self Self static struct super trait true type unsafe use where while]
|
|
70
74
|
},
|
|
71
75
|
java: {
|
|
72
76
|
extensions: %w[.java],
|
|
77
|
+
block_comment: true,
|
|
73
78
|
keywords: %w[abstract assert boolean break byte case catch char class const continue default do double else enum extends false final finally float for if implements import instanceof int interface long native new null package private protected public return short static strictfp super switch synchronized this throw throws transient true try void volatile while]
|
|
74
79
|
},
|
|
75
80
|
csharp: {
|
|
76
81
|
extensions: %w[.cs],
|
|
82
|
+
block_comment: true,
|
|
77
83
|
keywords: %w[abstract as base bool break byte case catch char checked class const continue decimal default delegate do double else enum event explicit extern false finally fixed float for foreach goto if implicit in int interface internal is lock long namespace new null object operator out override params private protected public readonly ref return sbyte sealed short sizeof stackalloc static string struct switch this throw true try typeof uint ulong unchecked unsafe ushort using virtual void volatile while var async await]
|
|
78
84
|
},
|
|
79
85
|
c: {
|
|
80
86
|
extensions: %w[.c .h],
|
|
87
|
+
block_comment: true,
|
|
81
88
|
keywords: %w[auto break case char const continue default do double else enum extern float for goto if inline int long register restrict return short signed sizeof static struct switch typedef union unsigned void volatile while]
|
|
82
89
|
},
|
|
83
90
|
cpp: {
|
|
84
91
|
extensions: %w[.cc .cpp .cxx .hpp .hh .hxx],
|
|
92
|
+
block_comment: true,
|
|
85
93
|
keywords: %w[alignas alignof and asm auto bool break case catch char char16_t char32_t class const constexpr const_cast continue decltype default delete do double dynamic_cast else enum explicit export extern false float for friend goto if inline int long mutable namespace new noexcept nullptr operator or private protected public register reinterpret_cast return short signed sizeof static static_assert static_cast struct switch template this throw true try typedef typeid typename union unsigned using virtual void volatile wchar_t while]
|
|
86
94
|
},
|
|
87
95
|
swift: {
|
|
88
96
|
extensions: %w[.swift],
|
|
97
|
+
block_comment: true,
|
|
89
98
|
keywords: %w[as associatedtype break case catch class continue default defer deinit do else enum extension false fileprivate for func guard if import in init inout internal is let nil open operator private protocol public repeat rethrows return self Self static struct subscript super switch throw throws true try typealias var where while]
|
|
90
99
|
},
|
|
91
100
|
kotlin: {
|
|
92
101
|
extensions: %w[.kt .kts],
|
|
102
|
+
block_comment: true,
|
|
93
103
|
keywords: %w[as break class continue do else false for fun if in interface is null object package return super this throw true try typealias typeof val var when while by catch constructor delegate dynamic field file finally get import init param property receiver set setparam where actual abstract annotation companion const crossinline data enum expect external final infix inline inner internal lateinit noinline open operator out override private protected public reified sealed suspend tailrec vararg]
|
|
94
104
|
},
|
|
95
105
|
lua: {
|
|
@@ -296,8 +306,7 @@ module Kward
|
|
|
296
306
|
return line.to_s unless definition
|
|
297
307
|
|
|
298
308
|
text = line.to_s
|
|
299
|
-
return colored(text, :gray) if editor_c_style_block_comment_line?(line_index)
|
|
300
|
-
|
|
309
|
+
return colored(text, :gray) if definition[:block_comment] && editor_c_style_block_comment_line?(line_index)
|
|
301
310
|
marker = definition[:line_comment] || "//"
|
|
302
311
|
comment_index = editor_comment_index(text, marker)
|
|
303
312
|
return editor_highlight_generic_code(text, definition[:keywords]) unless comment_index
|
|
@@ -313,7 +322,8 @@ module Kward
|
|
|
313
322
|
end
|
|
314
323
|
|
|
315
324
|
def editor_generic_pattern(keywords)
|
|
316
|
-
|
|
325
|
+
@editor_generic_patterns ||= {}
|
|
326
|
+
@editor_generic_patterns[keywords] ||= /("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`|\b\d+(?:\.\d+)?\b|\b[A-Z]\w*\b|\b(?:#{Regexp.union(keywords)})\b)/
|
|
317
327
|
end
|
|
318
328
|
|
|
319
329
|
def editor_highlight_generic_token(token, keywords)
|
|
@@ -333,16 +343,31 @@ module Kward
|
|
|
333
343
|
def editor_c_style_block_comment_line?(line_index)
|
|
334
344
|
return false unless line_index && @editor_state
|
|
335
345
|
|
|
336
|
-
|
|
337
|
-
@
|
|
346
|
+
lines = @editor_state.lines
|
|
347
|
+
unless @editor_block_comment_lines.equal?(lines)
|
|
348
|
+
@editor_block_comment_lines = lines
|
|
349
|
+
@editor_block_comment_states = []
|
|
350
|
+
@editor_block_comment_in_comment = false
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
target = line_index.to_i
|
|
354
|
+
return false if target.negative?
|
|
355
|
+
return @editor_block_comment_states[target] if target < @editor_block_comment_states.length
|
|
356
|
+
|
|
357
|
+
(@editor_block_comment_states.length..target).each do |index|
|
|
358
|
+
line = lines[index].to_s
|
|
338
359
|
starts_block = editor_comment_index(line, "/*")
|
|
339
|
-
ends_block =
|
|
340
|
-
|
|
360
|
+
ends_block = @editor_block_comment_in_comment && line.include?("*/")
|
|
361
|
+
@editor_block_comment_states << (@editor_block_comment_in_comment || !starts_block.nil?)
|
|
341
362
|
|
|
342
|
-
|
|
343
|
-
|
|
363
|
+
if starts_block && !line[starts_block..].to_s.include?("*/")
|
|
364
|
+
@editor_block_comment_in_comment = true
|
|
365
|
+
elsif ends_block
|
|
366
|
+
@editor_block_comment_in_comment = false
|
|
367
|
+
end
|
|
344
368
|
end
|
|
345
|
-
|
|
369
|
+
|
|
370
|
+
@editor_block_comment_states[target]
|
|
346
371
|
end
|
|
347
372
|
|
|
348
373
|
def editor_comment_index(line, marker)
|