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
|
@@ -102,19 +102,19 @@ module Kward
|
|
|
102
102
|
if editor_result
|
|
103
103
|
record_tab_transient_shell_output(editor_result.output, render: false)
|
|
104
104
|
@prompt.say(editor_result.output) unless editor_result.output.to_s.empty?
|
|
105
|
-
|
|
105
|
+
open_kwsh_editor(editor_result.open_editor_path, shell) if editor_result.open_editor_path
|
|
106
106
|
return true
|
|
107
107
|
end
|
|
108
108
|
|
|
109
109
|
expanded_command = shell.expand_alias(command, interactive: true)
|
|
110
|
-
run_user_interactive_pty_command(
|
|
110
|
+
result = run_user_interactive_pty_command(
|
|
111
111
|
expanded_command,
|
|
112
|
-
shell:
|
|
113
|
-
env: interactive_pty_environment(
|
|
112
|
+
shell: Kwsh::DEFAULT_SHELL,
|
|
113
|
+
env: interactive_pty_environment(shell.child_env, preserve_git_pager: true),
|
|
114
114
|
cwd: interactive_workspace_root(agent),
|
|
115
115
|
intro: "$ #{command}\n"
|
|
116
116
|
)
|
|
117
|
-
true
|
|
117
|
+
result&.tab_action ? :tab_action : true
|
|
118
118
|
end
|
|
119
119
|
|
|
120
120
|
def run_captured_shell_command(command, agent)
|
|
@@ -142,7 +142,7 @@ module Kward
|
|
|
142
142
|
completion = bang_shell(agent).complete(value[1..], cursor.to_i - 1)
|
|
143
143
|
return nil unless completion
|
|
144
144
|
|
|
145
|
-
|
|
145
|
+
Kwsh::Completion.new(
|
|
146
146
|
range: (completion.range.begin + 1)...(completion.range.end + 1),
|
|
147
147
|
replacement: completion.replacement,
|
|
148
148
|
candidates: completion.candidates
|
|
@@ -151,12 +151,14 @@ module Kward
|
|
|
151
151
|
|
|
152
152
|
def bang_shell(agent)
|
|
153
153
|
root = File.expand_path(interactive_workspace_root(agent).to_s)
|
|
154
|
-
|
|
155
|
-
|
|
154
|
+
config = ConfigFiles.read_kwsh_config
|
|
155
|
+
aliases = config[:aliases]
|
|
156
|
+
configured_env = config[:env]
|
|
157
|
+
cache_key = [root, ENV.fetch("PATH", ""), aliases.sort, configured_env.sort]
|
|
156
158
|
return @bang_shell if @bang_shell_key == cache_key
|
|
157
159
|
|
|
158
160
|
@bang_shell_key = cache_key
|
|
159
|
-
@bang_shell =
|
|
161
|
+
@bang_shell = Kwsh.new(cwd: root, env: ENV.to_h, configured_env: configured_env, aliases: aliases)
|
|
160
162
|
end
|
|
161
163
|
|
|
162
164
|
def install_bang_completion_provider(agent)
|
|
@@ -182,7 +184,7 @@ module Kward
|
|
|
182
184
|
@bang_completion_provider_installed = false
|
|
183
185
|
end
|
|
184
186
|
|
|
185
|
-
def
|
|
187
|
+
def run_kwsh(agent)
|
|
186
188
|
unless @prompt.respond_to?(:ask)
|
|
187
189
|
runtime_output("The embedded shell is only available in interactive mode.")
|
|
188
190
|
return
|
|
@@ -190,11 +192,11 @@ module Kward
|
|
|
190
192
|
|
|
191
193
|
tab = active_tab if respond_to?(:active_tab, true)
|
|
192
194
|
entering = tab.nil? || tab.shell.nil?
|
|
193
|
-
shell = tab&.shell ||
|
|
195
|
+
shell = tab&.shell || build_kwsh(agent)
|
|
194
196
|
tab.shell = shell if tab
|
|
195
197
|
shell_agent = shell_prompt_agent_for(agent, tab: tab)
|
|
196
|
-
runtime_output("Entering
|
|
197
|
-
|
|
198
|
+
runtime_output("Entering kwsh. Type exit or press Ctrl+D on an empty prompt to return.") if entering
|
|
199
|
+
run_kwsh_loop(shell, tab: tab, history: build_kwsh_history(agent), shell_agent: shell_agent)
|
|
198
200
|
end
|
|
199
201
|
|
|
200
202
|
def shell_prompt_agent_for(agent, tab: nil)
|
|
@@ -208,17 +210,20 @@ module Kward
|
|
|
208
210
|
|
|
209
211
|
def build_shell_prompt_agent(active_agent)
|
|
210
212
|
active_conversation = active_agent.conversation
|
|
213
|
+
config = ConfigFiles.read_config
|
|
214
|
+
configured_provider = ConfigFiles.shell_agent_provider(config)
|
|
211
215
|
conversation = Conversation.new(
|
|
212
216
|
system_message: ShellPrompt.system_message,
|
|
213
217
|
workspace_root: active_conversation.workspace_root,
|
|
214
|
-
provider: active_conversation.provider || current_model_provider,
|
|
215
|
-
model: active_conversation.model || current_model_id,
|
|
216
|
-
reasoning_effort: active_conversation.reasoning_effort || current_reasoning_effort
|
|
218
|
+
provider: configured_provider || active_conversation.provider || current_model_provider,
|
|
219
|
+
model: ConfigFiles.shell_agent_model(config) || (configured_provider ? nil : active_conversation.model || current_model_id),
|
|
220
|
+
reasoning_effort: ConfigFiles.shell_agent_reasoning_effort(config) || (configured_provider ? nil : active_conversation.reasoning_effort || current_reasoning_effort)
|
|
217
221
|
)
|
|
218
222
|
Agent.new(
|
|
219
223
|
client: @client,
|
|
220
224
|
tool_registry: active_agent.tool_registry,
|
|
221
225
|
conversation: conversation,
|
|
226
|
+
strict_provider: !configured_provider.nil?,
|
|
222
227
|
warning_sink: ConfigFiles.warning_sink
|
|
223
228
|
)
|
|
224
229
|
end
|
|
@@ -269,8 +274,8 @@ module Kward
|
|
|
269
274
|
input.to_s.start_with?("?")
|
|
270
275
|
end
|
|
271
276
|
|
|
272
|
-
def
|
|
273
|
-
config = ConfigFiles.
|
|
277
|
+
def build_kwsh(agent)
|
|
278
|
+
config = ConfigFiles.read_kwsh_config
|
|
274
279
|
PersistentShellSession.new(
|
|
275
280
|
cwd: interactive_workspace_root(agent),
|
|
276
281
|
configured_env: config[:env],
|
|
@@ -281,8 +286,8 @@ module Kward
|
|
|
281
286
|
)
|
|
282
287
|
end
|
|
283
288
|
|
|
284
|
-
def
|
|
285
|
-
config = ConfigFiles.
|
|
289
|
+
def build_kwsh_history(agent)
|
|
290
|
+
config = ConfigFiles.read_kwsh_config
|
|
286
291
|
PromptHistory.new(
|
|
287
292
|
cwd: interactive_workspace_root(agent),
|
|
288
293
|
limit: config[:history_limit],
|
|
@@ -297,7 +302,7 @@ module Kward
|
|
|
297
302
|
return
|
|
298
303
|
end
|
|
299
304
|
|
|
300
|
-
config = ConfigFiles.
|
|
305
|
+
config = ConfigFiles.read_kwsh_config
|
|
301
306
|
run_user_interactive_pty_command(
|
|
302
307
|
command,
|
|
303
308
|
shell: config[:shell],
|
|
@@ -314,8 +319,9 @@ module Kward
|
|
|
314
319
|
elsif @prompt.respond_to?(:say)
|
|
315
320
|
@prompt.say(intro_message)
|
|
316
321
|
end
|
|
322
|
+
origin_tab = active_tab if respond_to?(:active_tab, true)
|
|
317
323
|
result = run_interactive_pty_with_terminal_handoff(shell, command, env: env, cwd: cwd) do |sink, completed_result|
|
|
318
|
-
transcript_output = record_completed_pty_output(sink, completed_result)
|
|
324
|
+
transcript_output = record_completed_pty_output(sink, completed_result, tab: origin_tab)
|
|
319
325
|
yield(transcript_output, completed_result) if block_given?
|
|
320
326
|
end
|
|
321
327
|
refresh_composer_status
|
|
@@ -327,20 +333,29 @@ module Kward
|
|
|
327
333
|
|
|
328
334
|
def run_interactive_pty_with_terminal_handoff(shell, command, env:, cwd:, &on_complete)
|
|
329
335
|
runner = InteractivePtyRunner.new
|
|
336
|
+
origin_tab = active_tab if respond_to?(:active_tab, true)
|
|
330
337
|
with_interactive_terminal_handoff do |input, sink|
|
|
338
|
+
tab_action_handler = terminal_handoff_tab_action_handler
|
|
339
|
+
on_detach = if origin_tab
|
|
340
|
+
-> { BufferedPtyOutputSink.new(max_capture_bytes: Kward::CLI::Tabs::TRANSIENT_SHELL_OUTPUT_LIMIT) }
|
|
341
|
+
end
|
|
331
342
|
result = if shell.respond_to?(:run_interactive)
|
|
332
|
-
shell.run_interactive(command, input: input, sink: sink)
|
|
343
|
+
shell.run_interactive(command, input: input, sink: sink, tab_action_handler: tab_action_handler, on_detach: on_detach)
|
|
333
344
|
else
|
|
334
|
-
runner.run(shell, "-c", command, env: env, cwd: cwd, input: input, sink: sink)
|
|
345
|
+
runner.run(shell, "-c", command, env: env, cwd: cwd, input: input, sink: sink, tab_action_handler: tab_action_handler, on_detach: on_detach)
|
|
346
|
+
end
|
|
347
|
+
if result&.background
|
|
348
|
+
register_background_tab_run(origin_tab, result.background, &on_complete)
|
|
349
|
+
else
|
|
350
|
+
on_complete.call(sink, result) if on_complete
|
|
335
351
|
end
|
|
336
|
-
on_complete.call(sink, result) if on_complete
|
|
337
352
|
result
|
|
338
353
|
end
|
|
339
354
|
end
|
|
340
355
|
|
|
341
356
|
def with_interactive_terminal_handoff
|
|
342
357
|
if @prompt.respond_to?(:with_inline_terminal_handoff)
|
|
343
|
-
|
|
358
|
+
with_tab_keybinding_handoff(:with_inline_terminal_handoff) do |input, output, transition|
|
|
344
359
|
sink = AdaptivePtyOutputSink.new(
|
|
345
360
|
output: output,
|
|
346
361
|
on_exclusive: transition,
|
|
@@ -349,7 +364,7 @@ module Kward
|
|
|
349
364
|
yield(input, sink)
|
|
350
365
|
end
|
|
351
366
|
elsif @prompt.respond_to?(:with_terminal_handoff)
|
|
352
|
-
|
|
367
|
+
with_tab_keybinding_handoff(:with_terminal_handoff) do |input, output|
|
|
353
368
|
sink = PassthroughPtyOutputSink.new(
|
|
354
369
|
output: output,
|
|
355
370
|
max_capture_bytes: MAX_TRANSIENT_TERMINAL_OUTPUT_BYTES
|
|
@@ -365,7 +380,31 @@ module Kward
|
|
|
365
380
|
end
|
|
366
381
|
end
|
|
367
382
|
|
|
368
|
-
def
|
|
383
|
+
def with_tab_keybinding_handoff(method_name)
|
|
384
|
+
method = @prompt.method(method_name)
|
|
385
|
+
supports_option = method.parameters.any? do |type, name|
|
|
386
|
+
type == :keyrest || (type == :key || type == :keyreq) && name == :preserve_tab_keybindings
|
|
387
|
+
end
|
|
388
|
+
if supports_option
|
|
389
|
+
@prompt.public_send(method_name, preserve_tab_keybindings: true) { |*args| yield(*args) }
|
|
390
|
+
else
|
|
391
|
+
@prompt.public_send(method_name) { |*args| yield(*args) }
|
|
392
|
+
end
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
def terminal_handoff_tab_action_handler
|
|
396
|
+
return unless @prompt.respond_to?(:filter_terminal_handoff_input)
|
|
397
|
+
return unless respond_to?(:handle_tab_action, true)
|
|
398
|
+
|
|
399
|
+
lambda do |chunk|
|
|
400
|
+
filtered = @prompt.filter_terminal_handoff_input(chunk)
|
|
401
|
+
action = filtered[:tab_action]
|
|
402
|
+
handle_tab_action(action, @session_store) if action
|
|
403
|
+
filtered
|
|
404
|
+
end
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
def record_completed_pty_output(sink, result, tab: nil)
|
|
369
408
|
classified_output = sink.respond_to?(:transcript_safe?)
|
|
370
409
|
return if classified_output && !sink.transcript_safe?
|
|
371
410
|
|
|
@@ -382,8 +421,8 @@ module Kward
|
|
|
382
421
|
)
|
|
383
422
|
return unless transcript_output
|
|
384
423
|
|
|
385
|
-
record_tab_transient_shell_output(transcript_output, render: false)
|
|
386
|
-
if @prompt.respond_to?(:record_transient_terminal_output)
|
|
424
|
+
record_tab_transient_shell_output(transcript_output, render: false, tab: tab)
|
|
425
|
+
if @prompt.respond_to?(:record_transient_terminal_output) && (!tab || tab == active_tab)
|
|
387
426
|
@prompt.record_transient_terminal_output(transcript_output, render: false)
|
|
388
427
|
end
|
|
389
428
|
transcript_output
|
|
@@ -422,14 +461,14 @@ module Kward
|
|
|
422
461
|
end
|
|
423
462
|
end
|
|
424
463
|
|
|
425
|
-
def
|
|
464
|
+
def run_kwsh_loop(shell, tab: nil, history: nil, shell_agent: nil)
|
|
426
465
|
shell_agent ||= shell_prompt_agent_for(tab.agent, tab: tab) if tab&.agent
|
|
427
|
-
|
|
428
|
-
|
|
466
|
+
with_kwsh_history(history) do
|
|
467
|
+
run_kwsh_loop_with_history(shell, tab: tab, shell_agent: shell_agent)
|
|
429
468
|
end
|
|
430
469
|
end
|
|
431
470
|
|
|
432
|
-
def
|
|
471
|
+
def run_kwsh_loop_with_history(shell, tab: nil, shell_agent: nil)
|
|
433
472
|
pending_shell_inputs = []
|
|
434
473
|
loop do
|
|
435
474
|
if @prompt.respond_to?(:editing_file?) && @prompt.editing_file?
|
|
@@ -440,7 +479,7 @@ module Kward
|
|
|
440
479
|
end
|
|
441
480
|
end
|
|
442
481
|
|
|
443
|
-
input = pending_shell_inputs.shift ||
|
|
482
|
+
input = pending_shell_inputs.shift || ask_kwsh(shell)
|
|
444
483
|
if input.is_a?(Hash) && input[:tab_action]
|
|
445
484
|
(@pending_inputs ||= []).unshift(input)
|
|
446
485
|
return :tab_action
|
|
@@ -452,7 +491,7 @@ module Kward
|
|
|
452
491
|
next
|
|
453
492
|
end
|
|
454
493
|
|
|
455
|
-
result =
|
|
494
|
+
result = run_kwsh_command(shell, input)
|
|
456
495
|
if result.clear
|
|
457
496
|
clear_active_tab_transient_shell_output
|
|
458
497
|
@prompt.clear_transcript if @prompt.respond_to?(:clear_transcript)
|
|
@@ -463,37 +502,39 @@ module Kward
|
|
|
463
502
|
return :tab_action if pending_tab_action?
|
|
464
503
|
|
|
465
504
|
if result.open_editor_path
|
|
466
|
-
editor_result =
|
|
505
|
+
editor_result = open_kwsh_editor(result.open_editor_path, shell)
|
|
467
506
|
return :tab_action if editor_result == :tab_action
|
|
468
507
|
|
|
469
508
|
next
|
|
470
509
|
end
|
|
471
510
|
if result.interactive_command
|
|
472
|
-
|
|
511
|
+
pty_result = run_kwsh_interactive_pty_command(shell, result)
|
|
512
|
+
return :tab_action if pty_result&.tab_action
|
|
513
|
+
|
|
473
514
|
next
|
|
474
515
|
end
|
|
475
516
|
if result.exit_shell
|
|
476
|
-
|
|
517
|
+
close_kwsh_session(shell)
|
|
477
518
|
tab.shell = nil if tab
|
|
478
519
|
tab.shell_agent = nil if tab
|
|
479
520
|
runtime_output("Shell exited.")
|
|
480
521
|
return :exited
|
|
481
522
|
end
|
|
482
523
|
end
|
|
483
|
-
|
|
524
|
+
close_kwsh_session(shell)
|
|
484
525
|
tab.shell = nil if tab
|
|
485
526
|
tab.shell_agent = nil if tab
|
|
486
527
|
runtime_output("Shell exited.")
|
|
487
528
|
:exited
|
|
488
529
|
end
|
|
489
530
|
|
|
490
|
-
def
|
|
531
|
+
def close_kwsh_session(shell)
|
|
491
532
|
shell.close if shell.respond_to?(:close)
|
|
492
533
|
rescue StandardError
|
|
493
534
|
nil
|
|
494
535
|
end
|
|
495
536
|
|
|
496
|
-
def
|
|
537
|
+
def with_kwsh_history(history)
|
|
497
538
|
if history && @prompt.respond_to?(:with_prompt_history)
|
|
498
539
|
@prompt.with_prompt_history(history) { yield }
|
|
499
540
|
else
|
|
@@ -501,7 +542,7 @@ module Kward
|
|
|
501
542
|
end
|
|
502
543
|
end
|
|
503
544
|
|
|
504
|
-
def
|
|
545
|
+
def open_kwsh_editor(path, shell)
|
|
505
546
|
unless @prompt.respond_to?(:edit_file)
|
|
506
547
|
runtime_output("Integrated editor is unavailable in this prompt.")
|
|
507
548
|
return false
|
|
@@ -516,7 +557,7 @@ module Kward
|
|
|
516
557
|
result
|
|
517
558
|
end
|
|
518
559
|
|
|
519
|
-
def
|
|
560
|
+
def ask_kwsh(shell)
|
|
520
561
|
provider = ->(input, cursor) { shell.complete(input, cursor) }
|
|
521
562
|
if @prompt.respond_to?(:with_completion_provider)
|
|
522
563
|
@prompt.with_completion_provider(provider, slash_overlay: false) { @prompt.ask(shell.prompt_label) }
|
|
@@ -525,7 +566,7 @@ module Kward
|
|
|
525
566
|
end
|
|
526
567
|
end
|
|
527
568
|
|
|
528
|
-
def
|
|
569
|
+
def run_kwsh_interactive_pty_command(shell, result)
|
|
529
570
|
shell_target = shell.respond_to?(:run_interactive) ? shell : shell.command_shell
|
|
530
571
|
run_user_interactive_pty_command(
|
|
531
572
|
result.interactive_command,
|
|
@@ -541,12 +582,12 @@ module Kward
|
|
|
541
582
|
end
|
|
542
583
|
end
|
|
543
584
|
|
|
544
|
-
def
|
|
585
|
+
def run_kwsh_command(shell, input)
|
|
545
586
|
if @prompt.respond_to?(:begin_busy_input)
|
|
546
587
|
@prompt.begin_busy_input(shell.prompt_label, activity: "running")
|
|
547
588
|
end
|
|
548
589
|
if @prompt.respond_to?(:write_transcript_delta) && @prompt.respond_to?(:poll_input)
|
|
549
|
-
|
|
590
|
+
run_streaming_kwsh_command(shell, input)
|
|
550
591
|
elsif @prompt.respond_to?(:write_transcript_delta)
|
|
551
592
|
shell.run(input) { |chunk| @prompt.write_transcript_delta(chunk) }
|
|
552
593
|
else
|
|
@@ -556,45 +597,67 @@ module Kward
|
|
|
556
597
|
@prompt.finish_busy_input if @prompt.respond_to?(:finish_busy_input)
|
|
557
598
|
end
|
|
558
599
|
|
|
559
|
-
def
|
|
600
|
+
def run_streaming_kwsh_command(shell, input)
|
|
560
601
|
cancellation = Cancellation.new
|
|
561
602
|
chunks = Queue.new
|
|
562
603
|
queued_inputs = []
|
|
604
|
+
origin_tab = active_tab if respond_to?(:active_tab, true)
|
|
605
|
+
detached_sink = nil
|
|
563
606
|
result = nil
|
|
564
607
|
error = nil
|
|
565
608
|
worker = Thread.new do
|
|
566
|
-
result = shell.run(input, cancellation: cancellation)
|
|
609
|
+
result = shell.run(input, cancellation: cancellation) do |chunk|
|
|
610
|
+
if detached_sink
|
|
611
|
+
detached_sink.write(chunk)
|
|
612
|
+
else
|
|
613
|
+
chunks << chunk
|
|
614
|
+
end
|
|
615
|
+
end
|
|
567
616
|
rescue StandardError => e
|
|
568
617
|
error = e
|
|
569
618
|
end
|
|
570
619
|
worker.report_on_exception = false
|
|
571
620
|
|
|
572
621
|
while worker.alive?
|
|
573
|
-
|
|
622
|
+
drain_kwsh_chunks(chunks)
|
|
574
623
|
poll_result = collect_queued_input(queued_inputs)
|
|
575
624
|
if poll_result == PromptInterface::CANCEL_INPUT
|
|
576
625
|
cancellation.cancel!
|
|
577
626
|
elsif poll_result.is_a?(Hash) && poll_result[:tab_action]
|
|
578
627
|
(@pending_inputs ||= []).unshift(poll_result)
|
|
628
|
+
if origin_tab
|
|
629
|
+
detached_sink = BufferedPtyOutputSink.new(max_capture_bytes: Kward::CLI::Tabs::TRANSIENT_SHELL_OUTPUT_LIMIT)
|
|
630
|
+
background = DetachedRun.new(sink: detached_sink, canceler: -> { cancellation.cancel! }) do
|
|
631
|
+
worker.join
|
|
632
|
+
raise error if error
|
|
633
|
+
|
|
634
|
+
result
|
|
635
|
+
end
|
|
636
|
+
register_background_tab_run(origin_tab, background) do |_sink, completed_result|
|
|
637
|
+
record_tab_transient_shell_output(completed_result.output, render: false, tab: origin_tab)
|
|
638
|
+
@prompt.say(completed_result.output) if origin_tab == active_tab && completed_result.output.to_s != ""
|
|
639
|
+
end
|
|
640
|
+
return Kwsh::Result.new(output: "", exit_status: 0, streamed: true)
|
|
641
|
+
end
|
|
579
642
|
cancellation.cancel!
|
|
580
643
|
end
|
|
581
644
|
sleep 0.01
|
|
582
645
|
end
|
|
583
646
|
worker.join
|
|
584
|
-
|
|
647
|
+
drain_kwsh_chunks(chunks)
|
|
585
648
|
raise error if error
|
|
586
649
|
|
|
587
650
|
queued_inputs.reverse_each { |pending_input| (@pending_inputs ||= []).unshift(pending_input) }
|
|
588
651
|
result
|
|
589
652
|
end
|
|
590
653
|
|
|
591
|
-
def record_tab_transient_shell_output(text, render: true)
|
|
654
|
+
def record_tab_transient_shell_output(text, render: true, tab: nil)
|
|
592
655
|
value = text.to_s
|
|
593
656
|
return if value.empty?
|
|
594
657
|
|
|
595
|
-
tab
|
|
658
|
+
tab ||= active_tab if respond_to?(:active_tab, true)
|
|
596
659
|
tab&.append_transient_shell_entry(value)
|
|
597
|
-
if render && @prompt.respond_to?(:record_transient_terminal_output)
|
|
660
|
+
if render && @prompt.respond_to?(:record_transient_terminal_output) && tab == active_tab
|
|
598
661
|
@prompt.record_transient_terminal_output(value)
|
|
599
662
|
end
|
|
600
663
|
end
|
|
@@ -641,7 +704,7 @@ module Kward
|
|
|
641
704
|
after
|
|
642
705
|
end
|
|
643
706
|
|
|
644
|
-
def
|
|
707
|
+
def drain_kwsh_chunks(chunks)
|
|
645
708
|
loop do
|
|
646
709
|
@prompt.write_transcript_delta(chunks.pop(true))
|
|
647
710
|
rescue ThreadError
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
require_relative "../scratchpad_languages"
|
|
2
|
+
|
|
1
3
|
# Namespace for the Kward CLI agent runtime.
|
|
2
4
|
module Kward
|
|
3
5
|
# Command-line frontend that coordinates terminal interaction, sessions, tools, and model turns.
|
|
@@ -48,7 +50,7 @@ module Kward
|
|
|
48
50
|
open_project_files_browser
|
|
49
51
|
[true, nil]
|
|
50
52
|
when "shell"
|
|
51
|
-
|
|
53
|
+
run_kwsh(agent)
|
|
52
54
|
[true, nil]
|
|
53
55
|
when "capture"
|
|
54
56
|
run_captured_shell_command(argument, agent)
|
|
@@ -304,11 +306,16 @@ module Kward
|
|
|
304
306
|
end
|
|
305
307
|
|
|
306
308
|
def open_scratchpad_command(argument)
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
else
|
|
310
|
-
runtime_output("The scratchpad is only available in the interactive prompt.")
|
|
309
|
+
unless @prompt.respond_to?(:scratchpad)
|
|
310
|
+
return runtime_output("The scratchpad is only available in the interactive prompt.")
|
|
311
311
|
end
|
|
312
|
+
|
|
313
|
+
value = argument.to_s.strip
|
|
314
|
+
return runtime_output(ScratchpadLanguages.help_text) if value.casecmp("help").zero?
|
|
315
|
+
|
|
316
|
+
@prompt.scratchpad(ScratchpadLanguages.normalize(value))
|
|
317
|
+
rescue ArgumentError => e
|
|
318
|
+
runtime_output(e.message)
|
|
312
319
|
end
|
|
313
320
|
|
|
314
321
|
def queue_editor_prompt_action(result)
|
|
@@ -318,15 +325,6 @@ module Kward
|
|
|
318
325
|
@pending_inputs.unshift(result)
|
|
319
326
|
end
|
|
320
327
|
|
|
321
|
-
def scratchpad_language_argument(argument)
|
|
322
|
-
value = argument.to_s.strip.downcase
|
|
323
|
-
return :text if value.empty? || value == "text"
|
|
324
|
-
return :markdown if ["markdown", "md"].include?(value)
|
|
325
|
-
return :ruby if ["ruby", "rb"].include?(value)
|
|
326
|
-
|
|
327
|
-
:text
|
|
328
|
-
end
|
|
329
|
-
|
|
330
328
|
def open_project_files_browser
|
|
331
329
|
if @prompt.respond_to?(:open_project_browser)
|
|
332
330
|
@prompt.open_project_browser
|