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.
Files changed (95) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +68 -15
  3. data/CONTRIBUTING.md +74 -0
  4. data/Gemfile.lock +8 -2
  5. data/README.md +21 -1
  6. data/Rakefile +46 -2
  7. data/SECURITY.md +31 -0
  8. data/doc/api.md +4 -0
  9. data/doc/composer.md +1 -1
  10. data/doc/configuration.md +68 -21
  11. data/doc/editor.md +28 -13
  12. data/doc/files.md +8 -4
  13. data/doc/getting-started.md +3 -0
  14. data/doc/pan.md +19 -15
  15. data/doc/platform-support.md +48 -0
  16. data/doc/security.md +3 -2
  17. data/doc/shell.md +62 -45
  18. data/doc/troubleshooting.md +12 -2
  19. data/doc/usage.md +5 -5
  20. data/kward.gemspec +5 -4
  21. data/lib/kward/agent.rb +6 -3
  22. data/lib/kward/ansi.rb +110 -10
  23. data/lib/kward/cli/auth_commands.rb +34 -13
  24. data/lib/kward/cli/commands.rb +83 -62
  25. data/lib/kward/cli/doctor.rb +39 -17
  26. data/lib/kward/cli/hook_commands.rb +22 -12
  27. data/lib/kward/cli/interactive_turn.rb +48 -7
  28. data/lib/kward/cli/project_skills_commands.rb +8 -4
  29. data/lib/kward/cli/prompt_interface.rb +27 -0
  30. data/lib/kward/cli/rendering.rb +15 -9
  31. data/lib/kward/cli/runtime_helpers.rb +118 -55
  32. data/lib/kward/cli/slash_commands.rb +12 -14
  33. data/lib/kward/cli/tabs.rb +83 -12
  34. data/lib/kward/cli/tool_summaries.rb +14 -0
  35. data/lib/kward/cli.rb +45 -7
  36. data/lib/kward/cli_transcript_formatter.rb +11 -4
  37. data/lib/kward/config_files.rb +82 -68
  38. data/lib/kward/detached_run.rb +44 -0
  39. data/lib/kward/interactive_pty_runner.rb +102 -28
  40. data/lib/kward/{ekwsh.rb → kwsh.rb} +35 -16
  41. data/lib/kward/kwshrc.rb +233 -0
  42. data/lib/kward/markdown_code_block.rb +136 -0
  43. data/lib/kward/model/client.rb +34 -22
  44. data/lib/kward/model/provider_catalog.rb +5 -0
  45. data/lib/kward/model/stream_parser.rb +20 -4
  46. data/lib/kward/pan/index.html.erb +3 -3
  47. data/lib/kward/pan/server.rb +23 -3
  48. data/lib/kward/persistent_shell_session.rb +119 -26
  49. data/lib/kward/project_files.rb +2 -2
  50. data/lib/kward/prompt_interface/composer_renderer.rb +44 -40
  51. data/lib/kward/prompt_interface/composer_state.rb +33 -24
  52. data/lib/kward/prompt_interface/editor/auto_indent.rb +24 -22
  53. data/lib/kward/prompt_interface/editor/controller.rb +30 -33
  54. data/lib/kward/prompt_interface/editor/endwise.rb +13 -4
  55. data/lib/kward/prompt_interface/editor/modes/vibe.rb +289 -44
  56. data/lib/kward/prompt_interface/editor/renderer.rb +108 -6
  57. data/lib/kward/prompt_interface/editor/runner.rb +362 -0
  58. data/lib/kward/prompt_interface/editor/runner_state.rb +78 -0
  59. data/lib/kward/prompt_interface/editor/state.rb +10 -10
  60. data/lib/kward/prompt_interface/editor/syntax_highlighter.rb +68 -6
  61. data/lib/kward/prompt_interface/editor/vibe_state.rb +3 -3
  62. data/lib/kward/prompt_interface/file_overlay.rb +71 -15
  63. data/lib/kward/prompt_interface/key_handler.rb +67 -0
  64. data/lib/kward/prompt_interface/overlay_renderer.rb +7 -5
  65. data/lib/kward/prompt_interface/project_browser.rb +415 -14
  66. data/lib/kward/prompt_interface/runtime_state.rb +50 -1
  67. data/lib/kward/prompt_interface/screen.rb +2 -2
  68. data/lib/kward/prompt_interface/selection_prompt.rb +3 -1
  69. data/lib/kward/prompt_interface/slash_overlay.rb +19 -4
  70. data/lib/kward/prompt_interface/transcript_renderer.rb +12 -7
  71. data/lib/kward/prompt_interface.rb +93 -18
  72. data/lib/kward/prompts/commands.rb +1 -1
  73. data/lib/kward/prompts.rb +1 -1
  74. data/lib/kward/pty_output_sink.rb +47 -0
  75. data/lib/kward/rpc/transcript_normalizer.rb +7 -3
  76. data/lib/kward/scratchpad_languages.rb +74 -0
  77. data/lib/kward/scratchpad_runner.rb +155 -29
  78. data/lib/kward/shell_prompt.rb +2 -0
  79. data/lib/kward/terminal_keys.rb +12 -0
  80. data/lib/kward/terminal_text.rb +121 -0
  81. data/lib/kward/text_matcher.rb +18 -0
  82. data/lib/kward/tools/open_editor.rb +41 -0
  83. data/lib/kward/tools/registry.rb +13 -4
  84. data/lib/kward/tools/tool_call.rb +2 -1
  85. data/lib/kward/version.rb +1 -1
  86. data/templates/default/fulldoc/html/css/kward.css +0 -125
  87. data/templates/default/fulldoc/html/images/kward_workflow.svg +52 -0
  88. data/templates/default/fulldoc/html/setup.rb +1 -1
  89. data/templates/default/kward_navigation.rb +1 -0
  90. data/templates/default/layout/html/footer.erb +10 -0
  91. data/templates/default/layout/html/headers.erb +23 -0
  92. data/templates/default/layout/html/layout.erb +6 -18
  93. data/templates/default/layout/html/setup.rb +41 -2
  94. metadata +36 -8
  95. 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
- open_ekwsh_editor(editor_result.open_editor_path, shell) if editor_result.open_editor_path
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: Ekwsh::DEFAULT_SHELL,
113
- env: interactive_pty_environment({}, preserve_git_pager: true),
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
- Ekwsh::Completion.new(
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
- aliases = ConfigFiles.read_ekwsh_config[:aliases]
155
- cache_key = [root, ENV.fetch("PATH", ""), aliases.sort]
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 = Ekwsh.new(cwd: root, env: ENV.to_h, aliases: aliases)
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 run_ekwsh(agent)
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 || build_ekwsh(agent)
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 ekwsh. Type exit or press Ctrl+D on an empty prompt to return.") if entering
197
- run_ekwsh_loop(shell, tab: tab, history: build_ekwsh_history(agent), shell_agent: shell_agent)
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 build_ekwsh(agent)
273
- config = ConfigFiles.read_ekwsh_config
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 build_ekwsh_history(agent)
285
- config = ConfigFiles.read_ekwsh_config
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.read_ekwsh_config
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
- @prompt.with_inline_terminal_handoff do |input, output, transition|
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
- @prompt.with_terminal_handoff do |input, output|
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 record_completed_pty_output(sink, result)
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 run_ekwsh_loop(shell, tab: nil, history: nil, shell_agent: nil)
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
- with_ekwsh_history(history) do
428
- run_ekwsh_loop_with_history(shell, tab: tab, shell_agent: shell_agent)
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 run_ekwsh_loop_with_history(shell, tab: nil, shell_agent: nil)
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 || ask_ekwsh(shell)
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 = run_ekwsh_command(shell, input)
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 = open_ekwsh_editor(result.open_editor_path, shell)
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
- run_ekwsh_interactive_pty_command(shell, result)
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
- close_ekwsh_session(shell)
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
- close_ekwsh_session(shell)
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 close_ekwsh_session(shell)
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 with_ekwsh_history(history)
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 open_ekwsh_editor(path, shell)
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 ask_ekwsh(shell)
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 run_ekwsh_interactive_pty_command(shell, result)
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 run_ekwsh_command(shell, input)
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
- run_streaming_ekwsh_command(shell, input)
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 run_streaming_ekwsh_command(shell, input)
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) { |chunk| chunks << chunk }
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
- drain_ekwsh_chunks(chunks)
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
- drain_ekwsh_chunks(chunks)
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 = active_tab if respond_to?(:active_tab, true)
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 drain_ekwsh_chunks(chunks)
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
- run_ekwsh(agent)
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
- if @prompt.respond_to?(:scratchpad)
308
- @prompt.scratchpad(scratchpad_language_argument(argument))
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