kward 0.82.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 +96 -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 +1 -0
- data/doc/api.md +4 -0
- data/doc/composer.md +2 -2
- data/doc/configuration.md +82 -20
- data/doc/editor.md +47 -22
- data/doc/files.md +12 -7
- data/doc/getting-started.md +3 -0
- data/doc/pan.md +19 -15
- data/doc/permissions.md +1 -0
- data/doc/platform-support.md +48 -0
- data/doc/releasing.md +12 -4
- data/doc/rpc.md +2 -2
- data/doc/sandboxing.md +5 -1
- data/doc/security.md +4 -3
- data/doc/shell.md +101 -60
- data/doc/tabs.md +3 -0
- data/doc/troubleshooting.md +12 -2
- data/doc/usage.md +7 -6
- data/kward.gemspec +5 -4
- data/lib/kward/agent.rb +7 -3
- data/lib/kward/ansi.rb +110 -10
- data/lib/kward/auth/anthropic_oauth.rb +7 -7
- 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 +144 -21
- 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 +260 -55
- data/lib/kward/cli/slash_commands.rb +21 -12
- data/lib/kward/cli/tabs.rb +91 -13
- data/lib/kward/cli/tool_summaries.rb +14 -0
- data/lib/kward/cli.rb +55 -7
- data/lib/kward/cli_transcript_formatter.rb +11 -4
- data/lib/kward/compaction/token_estimator.rb +12 -6
- data/lib/kward/config_files.rb +94 -63
- data/lib/kward/detached_run.rb +44 -0
- data/lib/kward/editor_prompt.rb +46 -0
- data/lib/kward/editor_prompt_session.rb +28 -0
- data/lib/kward/interactive_pty_runner.rb +102 -28
- data/lib/kward/{ekwsh.rb → kwsh.rb} +103 -24
- data/lib/kward/kwshrc.rb +233 -0
- data/lib/kward/markdown_code_block.rb +136 -0
- data/lib/kward/model/client.rb +35 -39
- data/lib/kward/model/model_info.rb +3 -2
- data/lib/kward/model/payloads.rb +0 -2
- 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 +843 -0
- data/lib/kward/project_files.rb +19 -6
- data/lib/kward/prompt_interface/composer_renderer.rb +45 -41
- data/lib/kward/prompt_interface/composer_state.rb +33 -24
- data/lib/kward/prompt_interface/editor/auto_indent.rb +27 -22
- data/lib/kward/prompt_interface/editor/controller.rb +99 -33
- data/lib/kward/prompt_interface/editor/endwise.rb +13 -4
- data/lib/kward/prompt_interface/editor/modes/modern.rb +4 -0
- data/lib/kward/prompt_interface/editor/modes/vibe.rb +298 -47
- data/lib/kward/prompt_interface/editor/renderer.rb +109 -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 +21 -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/editor/word_completion.rb +124 -0
- data/lib/kward/prompt_interface/file_overlay.rb +92 -22
- data/lib/kward/prompt_interface/key_handler.rb +75 -0
- data/lib/kward/prompt_interface/overlay_renderer.rb +7 -5
- data/lib/kward/prompt_interface/project_browser.rb +443 -17
- data/lib/kward/prompt_interface/runtime_state.rb +55 -2
- 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 +169 -18
- data/lib/kward/prompts/commands.rb +2 -1
- data/lib/kward/prompts.rb +1 -1
- data/lib/kward/pty_output_sink.rb +47 -0
- data/lib/kward/rpc/auth_manager.rb +1 -1
- data/lib/kward/rpc/server.rb +2 -1
- 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 +52 -0
- data/lib/kward/shell_prompt_session.rb +58 -0
- data/lib/kward/terminal_keys.rb +13 -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/prepare_shell_command.rb +28 -0
- data/lib/kward/tools/registry.rb +75 -8
- data/lib/kward/tools/replace_editor_buffer.rb +30 -0
- data/lib/kward/tools/run_shell_command.rb +24 -6
- 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 +44 -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,15 +192,91 @@ 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
|
-
|
|
196
|
-
|
|
197
|
+
shell_agent = shell_prompt_agent_for(agent, tab: tab)
|
|
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)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def shell_prompt_agent_for(agent, tab: nil)
|
|
203
|
+
return tab.shell_agent if tab&.shell_agent
|
|
204
|
+
return nil unless agent.respond_to?(:conversation) && agent.respond_to?(:tool_registry)
|
|
205
|
+
|
|
206
|
+
shell_agent = build_shell_prompt_agent(agent)
|
|
207
|
+
tab.shell_agent = shell_agent if tab
|
|
208
|
+
shell_agent
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def build_shell_prompt_agent(active_agent)
|
|
212
|
+
active_conversation = active_agent.conversation
|
|
213
|
+
config = ConfigFiles.read_config
|
|
214
|
+
configured_provider = ConfigFiles.shell_agent_provider(config)
|
|
215
|
+
conversation = Conversation.new(
|
|
216
|
+
system_message: ShellPrompt.system_message,
|
|
217
|
+
workspace_root: active_conversation.workspace_root,
|
|
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)
|
|
221
|
+
)
|
|
222
|
+
Agent.new(
|
|
223
|
+
client: @client,
|
|
224
|
+
tool_registry: active_agent.tool_registry,
|
|
225
|
+
conversation: conversation,
|
|
226
|
+
strict_provider: !configured_provider.nil?,
|
|
227
|
+
warning_sink: ConfigFiles.warning_sink
|
|
228
|
+
)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def run_shell_prompt_turn(input, shell, shell_agent)
|
|
232
|
+
tab = active_tab if respond_to?(:active_tab, true)
|
|
233
|
+
transcript_before = prompt_transcript_text
|
|
234
|
+
instruction = input.to_s.sub(/\A\?\s*/, "").strip
|
|
235
|
+
if instruction.empty?
|
|
236
|
+
runtime_output("Shell prompt instruction required after ?")
|
|
237
|
+
return []
|
|
238
|
+
end
|
|
239
|
+
unless shell_agent.respond_to?(:tool_registry)
|
|
240
|
+
runtime_output("Shell agent is unavailable in this prompt.")
|
|
241
|
+
return []
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
session = ShellPromptSession.new(shell)
|
|
245
|
+
registry = shell_agent.tool_registry.for_shell_prompt(session)
|
|
246
|
+
on_complete = lambda do |successful:, **|
|
|
247
|
+
next unless successful
|
|
248
|
+
next unless session.prepared_command
|
|
249
|
+
|
|
250
|
+
if @prompt.respond_to?(:prefill_input)
|
|
251
|
+
@prompt.prefill_input(session.prepared_command)
|
|
252
|
+
else
|
|
253
|
+
runtime_output("Prepared shell command:\n#{session.prepared_command}")
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
pending_inputs = run_interactive_turn(
|
|
257
|
+
shell_agent,
|
|
258
|
+
ShellPrompt.input(instruction, session.context),
|
|
259
|
+
display_input: "? #{instruction}",
|
|
260
|
+
tool_registry: registry,
|
|
261
|
+
busy_label: shell.prompt_label,
|
|
262
|
+
on_complete: on_complete
|
|
263
|
+
)
|
|
264
|
+
@prompt.update_prompt_label(shell.prompt_label) if @prompt.respond_to?(:update_prompt_label)
|
|
265
|
+
pending_inputs
|
|
266
|
+
rescue StandardError => e
|
|
267
|
+
runtime_output("Error: #{e.message}")
|
|
268
|
+
[]
|
|
269
|
+
ensure
|
|
270
|
+
record_shell_prompt_transcript(tab, transcript_before)
|
|
197
271
|
end
|
|
198
272
|
|
|
199
|
-
def
|
|
200
|
-
|
|
201
|
-
|
|
273
|
+
def shell_prompt_input?(input)
|
|
274
|
+
input.to_s.start_with?("?")
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def build_kwsh(agent)
|
|
278
|
+
config = ConfigFiles.read_kwsh_config
|
|
279
|
+
PersistentShellSession.new(
|
|
202
280
|
cwd: interactive_workspace_root(agent),
|
|
203
281
|
configured_env: config[:env],
|
|
204
282
|
aliases: config[:aliases],
|
|
@@ -208,8 +286,8 @@ module Kward
|
|
|
208
286
|
)
|
|
209
287
|
end
|
|
210
288
|
|
|
211
|
-
def
|
|
212
|
-
config = ConfigFiles.
|
|
289
|
+
def build_kwsh_history(agent)
|
|
290
|
+
config = ConfigFiles.read_kwsh_config
|
|
213
291
|
PromptHistory.new(
|
|
214
292
|
cwd: interactive_workspace_root(agent),
|
|
215
293
|
limit: config[:history_limit],
|
|
@@ -224,7 +302,7 @@ module Kward
|
|
|
224
302
|
return
|
|
225
303
|
end
|
|
226
304
|
|
|
227
|
-
config = ConfigFiles.
|
|
305
|
+
config = ConfigFiles.read_kwsh_config
|
|
228
306
|
run_user_interactive_pty_command(
|
|
229
307
|
command,
|
|
230
308
|
shell: config[:shell],
|
|
@@ -241,8 +319,10 @@ module Kward
|
|
|
241
319
|
elsif @prompt.respond_to?(:say)
|
|
242
320
|
@prompt.say(intro_message)
|
|
243
321
|
end
|
|
322
|
+
origin_tab = active_tab if respond_to?(:active_tab, true)
|
|
244
323
|
result = run_interactive_pty_with_terminal_handoff(shell, command, env: env, cwd: cwd) do |sink, completed_result|
|
|
245
|
-
record_completed_pty_output(sink, completed_result)
|
|
324
|
+
transcript_output = record_completed_pty_output(sink, completed_result, tab: origin_tab)
|
|
325
|
+
yield(transcript_output, completed_result) if block_given?
|
|
246
326
|
end
|
|
247
327
|
refresh_composer_status
|
|
248
328
|
result
|
|
@@ -253,39 +333,78 @@ module Kward
|
|
|
253
333
|
|
|
254
334
|
def run_interactive_pty_with_terminal_handoff(shell, command, env:, cwd:, &on_complete)
|
|
255
335
|
runner = InteractivePtyRunner.new
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
336
|
+
origin_tab = active_tab if respond_to?(:active_tab, true)
|
|
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
|
|
342
|
+
result = if shell.respond_to?(:run_interactive)
|
|
343
|
+
shell.run_interactive(command, input: input, sink: sink, tab_action_handler: tab_action_handler, on_detach: on_detach)
|
|
344
|
+
else
|
|
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
|
|
351
|
+
end
|
|
259
352
|
result
|
|
260
353
|
end
|
|
354
|
+
end
|
|
261
355
|
|
|
356
|
+
def with_interactive_terminal_handoff
|
|
262
357
|
if @prompt.respond_to?(:with_inline_terminal_handoff)
|
|
263
|
-
|
|
358
|
+
with_tab_keybinding_handoff(:with_inline_terminal_handoff) do |input, output, transition|
|
|
264
359
|
sink = AdaptivePtyOutputSink.new(
|
|
265
360
|
output: output,
|
|
266
361
|
on_exclusive: transition,
|
|
267
362
|
max_capture_bytes: MAX_TRANSIENT_TERMINAL_OUTPUT_BYTES
|
|
268
363
|
)
|
|
269
|
-
|
|
364
|
+
yield(input, sink)
|
|
270
365
|
end
|
|
271
366
|
elsif @prompt.respond_to?(:with_terminal_handoff)
|
|
272
|
-
|
|
367
|
+
with_tab_keybinding_handoff(:with_terminal_handoff) do |input, output|
|
|
273
368
|
sink = PassthroughPtyOutputSink.new(
|
|
274
369
|
output: output,
|
|
275
370
|
max_capture_bytes: MAX_TRANSIENT_TERMINAL_OUTPUT_BYTES
|
|
276
371
|
)
|
|
277
|
-
|
|
372
|
+
yield(input, sink)
|
|
278
373
|
end
|
|
279
374
|
else
|
|
280
375
|
sink = PassthroughPtyOutputSink.new(
|
|
281
376
|
output: $stdout,
|
|
282
377
|
max_capture_bytes: MAX_TRANSIENT_TERMINAL_OUTPUT_BYTES
|
|
283
378
|
)
|
|
284
|
-
|
|
379
|
+
yield($stdin, sink)
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
|
|
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
|
|
285
404
|
end
|
|
286
405
|
end
|
|
287
406
|
|
|
288
|
-
def record_completed_pty_output(sink, result)
|
|
407
|
+
def record_completed_pty_output(sink, result, tab: nil)
|
|
289
408
|
classified_output = sink.respond_to?(:transcript_safe?)
|
|
290
409
|
return if classified_output && !sink.transcript_safe?
|
|
291
410
|
|
|
@@ -302,10 +421,11 @@ module Kward
|
|
|
302
421
|
)
|
|
303
422
|
return unless transcript_output
|
|
304
423
|
|
|
305
|
-
record_tab_transient_shell_output(transcript_output, render: false)
|
|
306
|
-
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)
|
|
307
426
|
@prompt.record_transient_terminal_output(transcript_output, render: false)
|
|
308
427
|
end
|
|
428
|
+
transcript_output
|
|
309
429
|
end
|
|
310
430
|
|
|
311
431
|
def terminal_transcript_output(
|
|
@@ -341,13 +461,15 @@ module Kward
|
|
|
341
461
|
end
|
|
342
462
|
end
|
|
343
463
|
|
|
344
|
-
def
|
|
345
|
-
|
|
346
|
-
|
|
464
|
+
def run_kwsh_loop(shell, tab: nil, history: nil, shell_agent: nil)
|
|
465
|
+
shell_agent ||= shell_prompt_agent_for(tab.agent, tab: tab) if tab&.agent
|
|
466
|
+
with_kwsh_history(history) do
|
|
467
|
+
run_kwsh_loop_with_history(shell, tab: tab, shell_agent: shell_agent)
|
|
347
468
|
end
|
|
348
469
|
end
|
|
349
470
|
|
|
350
|
-
def
|
|
471
|
+
def run_kwsh_loop_with_history(shell, tab: nil, shell_agent: nil)
|
|
472
|
+
pending_shell_inputs = []
|
|
351
473
|
loop do
|
|
352
474
|
if @prompt.respond_to?(:editing_file?) && @prompt.editing_file?
|
|
353
475
|
editor_result = @prompt.run_editor
|
|
@@ -357,14 +479,19 @@ module Kward
|
|
|
357
479
|
end
|
|
358
480
|
end
|
|
359
481
|
|
|
360
|
-
input =
|
|
482
|
+
input = pending_shell_inputs.shift || ask_kwsh(shell)
|
|
361
483
|
if input.is_a?(Hash) && input[:tab_action]
|
|
362
484
|
(@pending_inputs ||= []).unshift(input)
|
|
363
485
|
return :tab_action
|
|
364
486
|
end
|
|
365
487
|
break if input.nil?
|
|
366
488
|
|
|
367
|
-
|
|
489
|
+
if shell_prompt_input?(input)
|
|
490
|
+
pending_shell_inputs.concat(run_shell_prompt_turn(input, shell, shell_agent))
|
|
491
|
+
next
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
result = run_kwsh_command(shell, input)
|
|
368
495
|
if result.clear
|
|
369
496
|
clear_active_tab_transient_shell_output
|
|
370
497
|
@prompt.clear_transcript if @prompt.respond_to?(:clear_transcript)
|
|
@@ -375,27 +502,39 @@ module Kward
|
|
|
375
502
|
return :tab_action if pending_tab_action?
|
|
376
503
|
|
|
377
504
|
if result.open_editor_path
|
|
378
|
-
editor_result =
|
|
505
|
+
editor_result = open_kwsh_editor(result.open_editor_path, shell)
|
|
379
506
|
return :tab_action if editor_result == :tab_action
|
|
380
507
|
|
|
381
508
|
next
|
|
382
509
|
end
|
|
383
510
|
if result.interactive_command
|
|
384
|
-
|
|
511
|
+
pty_result = run_kwsh_interactive_pty_command(shell, result)
|
|
512
|
+
return :tab_action if pty_result&.tab_action
|
|
513
|
+
|
|
385
514
|
next
|
|
386
515
|
end
|
|
387
516
|
if result.exit_shell
|
|
517
|
+
close_kwsh_session(shell)
|
|
388
518
|
tab.shell = nil if tab
|
|
519
|
+
tab.shell_agent = nil if tab
|
|
389
520
|
runtime_output("Shell exited.")
|
|
390
521
|
return :exited
|
|
391
522
|
end
|
|
392
523
|
end
|
|
524
|
+
close_kwsh_session(shell)
|
|
393
525
|
tab.shell = nil if tab
|
|
526
|
+
tab.shell_agent = nil if tab
|
|
394
527
|
runtime_output("Shell exited.")
|
|
395
528
|
:exited
|
|
396
529
|
end
|
|
397
530
|
|
|
398
|
-
def
|
|
531
|
+
def close_kwsh_session(shell)
|
|
532
|
+
shell.close if shell.respond_to?(:close)
|
|
533
|
+
rescue StandardError
|
|
534
|
+
nil
|
|
535
|
+
end
|
|
536
|
+
|
|
537
|
+
def with_kwsh_history(history)
|
|
399
538
|
if history && @prompt.respond_to?(:with_prompt_history)
|
|
400
539
|
@prompt.with_prompt_history(history) { yield }
|
|
401
540
|
else
|
|
@@ -403,7 +542,7 @@ module Kward
|
|
|
403
542
|
end
|
|
404
543
|
end
|
|
405
544
|
|
|
406
|
-
def
|
|
545
|
+
def open_kwsh_editor(path, shell)
|
|
407
546
|
unless @prompt.respond_to?(:edit_file)
|
|
408
547
|
runtime_output("Integrated editor is unavailable in this prompt.")
|
|
409
548
|
return false
|
|
@@ -418,7 +557,7 @@ module Kward
|
|
|
418
557
|
result
|
|
419
558
|
end
|
|
420
559
|
|
|
421
|
-
def
|
|
560
|
+
def ask_kwsh(shell)
|
|
422
561
|
provider = ->(input, cursor) { shell.complete(input, cursor) }
|
|
423
562
|
if @prompt.respond_to?(:with_completion_provider)
|
|
424
563
|
@prompt.with_completion_provider(provider, slash_overlay: false) { @prompt.ask(shell.prompt_label) }
|
|
@@ -427,22 +566,28 @@ module Kward
|
|
|
427
566
|
end
|
|
428
567
|
end
|
|
429
568
|
|
|
430
|
-
def
|
|
569
|
+
def run_kwsh_interactive_pty_command(shell, result)
|
|
570
|
+
shell_target = shell.respond_to?(:run_interactive) ? shell : shell.command_shell
|
|
431
571
|
run_user_interactive_pty_command(
|
|
432
572
|
result.interactive_command,
|
|
433
|
-
shell:
|
|
573
|
+
shell: shell_target,
|
|
434
574
|
env: shell.child_env(interactive: true),
|
|
435
575
|
cwd: shell.cwd,
|
|
436
576
|
intro: result.output
|
|
437
|
-
)
|
|
577
|
+
) do |transcript_output, completed_result|
|
|
578
|
+
shell.record_interactive_result(
|
|
579
|
+
output: transcript_output,
|
|
580
|
+
exit_status: completed_result.exit_status
|
|
581
|
+
)
|
|
582
|
+
end
|
|
438
583
|
end
|
|
439
584
|
|
|
440
|
-
def
|
|
585
|
+
def run_kwsh_command(shell, input)
|
|
441
586
|
if @prompt.respond_to?(:begin_busy_input)
|
|
442
587
|
@prompt.begin_busy_input(shell.prompt_label, activity: "running")
|
|
443
588
|
end
|
|
444
589
|
if @prompt.respond_to?(:write_transcript_delta) && @prompt.respond_to?(:poll_input)
|
|
445
|
-
|
|
590
|
+
run_streaming_kwsh_command(shell, input)
|
|
446
591
|
elsif @prompt.respond_to?(:write_transcript_delta)
|
|
447
592
|
shell.run(input) { |chunk| @prompt.write_transcript_delta(chunk) }
|
|
448
593
|
else
|
|
@@ -452,45 +597,67 @@ module Kward
|
|
|
452
597
|
@prompt.finish_busy_input if @prompt.respond_to?(:finish_busy_input)
|
|
453
598
|
end
|
|
454
599
|
|
|
455
|
-
def
|
|
600
|
+
def run_streaming_kwsh_command(shell, input)
|
|
456
601
|
cancellation = Cancellation.new
|
|
457
602
|
chunks = Queue.new
|
|
458
603
|
queued_inputs = []
|
|
604
|
+
origin_tab = active_tab if respond_to?(:active_tab, true)
|
|
605
|
+
detached_sink = nil
|
|
459
606
|
result = nil
|
|
460
607
|
error = nil
|
|
461
608
|
worker = Thread.new do
|
|
462
|
-
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
|
|
463
616
|
rescue StandardError => e
|
|
464
617
|
error = e
|
|
465
618
|
end
|
|
466
619
|
worker.report_on_exception = false
|
|
467
620
|
|
|
468
621
|
while worker.alive?
|
|
469
|
-
|
|
622
|
+
drain_kwsh_chunks(chunks)
|
|
470
623
|
poll_result = collect_queued_input(queued_inputs)
|
|
471
624
|
if poll_result == PromptInterface::CANCEL_INPUT
|
|
472
625
|
cancellation.cancel!
|
|
473
626
|
elsif poll_result.is_a?(Hash) && poll_result[:tab_action]
|
|
474
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
|
|
475
642
|
cancellation.cancel!
|
|
476
643
|
end
|
|
477
644
|
sleep 0.01
|
|
478
645
|
end
|
|
479
646
|
worker.join
|
|
480
|
-
|
|
647
|
+
drain_kwsh_chunks(chunks)
|
|
481
648
|
raise error if error
|
|
482
649
|
|
|
483
650
|
queued_inputs.reverse_each { |pending_input| (@pending_inputs ||= []).unshift(pending_input) }
|
|
484
651
|
result
|
|
485
652
|
end
|
|
486
653
|
|
|
487
|
-
def record_tab_transient_shell_output(text, render: true)
|
|
654
|
+
def record_tab_transient_shell_output(text, render: true, tab: nil)
|
|
488
655
|
value = text.to_s
|
|
489
656
|
return if value.empty?
|
|
490
657
|
|
|
491
|
-
tab
|
|
658
|
+
tab ||= active_tab if respond_to?(:active_tab, true)
|
|
492
659
|
tab&.append_transient_shell_entry(value)
|
|
493
|
-
if render && @prompt.respond_to?(:record_transient_terminal_output)
|
|
660
|
+
if render && @prompt.respond_to?(:record_transient_terminal_output) && tab == active_tab
|
|
494
661
|
@prompt.record_transient_terminal_output(value)
|
|
495
662
|
end
|
|
496
663
|
end
|
|
@@ -499,7 +666,45 @@ module Kward
|
|
|
499
666
|
clear_active_tab_transient_shell_entries if respond_to?(:clear_active_tab_transient_shell_entries, true)
|
|
500
667
|
end
|
|
501
668
|
|
|
502
|
-
def
|
|
669
|
+
def prompt_transcript_text
|
|
670
|
+
return nil unless @prompt.respond_to?(:tab_view_snapshot)
|
|
671
|
+
|
|
672
|
+
snapshot = @prompt.tab_view_snapshot
|
|
673
|
+
transcript = snapshot[:transcript_buffer] || snapshot["transcript_buffer"] || snapshot[:transcript] || snapshot["transcript"]
|
|
674
|
+
transcript = transcript.map(&:to_s).join if transcript.is_a?(Array)
|
|
675
|
+
transcript.to_s
|
|
676
|
+
rescue StandardError
|
|
677
|
+
nil
|
|
678
|
+
end
|
|
679
|
+
|
|
680
|
+
def record_shell_prompt_transcript(tab, transcript_before)
|
|
681
|
+
return unless tab && !transcript_before.nil?
|
|
682
|
+
|
|
683
|
+
transcript_after = prompt_transcript_text
|
|
684
|
+
return if transcript_after.nil?
|
|
685
|
+
|
|
686
|
+
delta = shell_prompt_transcript_delta(transcript_before, transcript_after)
|
|
687
|
+
tab.append_transient_shell_entry(delta) unless delta.empty?
|
|
688
|
+
rescue StandardError
|
|
689
|
+
nil
|
|
690
|
+
end
|
|
691
|
+
|
|
692
|
+
def shell_prompt_transcript_delta(before, after)
|
|
693
|
+
return "" if before == after
|
|
694
|
+
return after[before.length..].to_s if after.start_with?(before)
|
|
695
|
+
|
|
696
|
+
# The prompt transcript is bounded and may trim while a large agent
|
|
697
|
+
# response is rendered. Preserve only the part not already retained.
|
|
698
|
+
overlap = [before.length, after.length].min
|
|
699
|
+
while overlap.positive?
|
|
700
|
+
return after[overlap..].to_s if before.end_with?(after[0, overlap])
|
|
701
|
+
|
|
702
|
+
overlap -= 1
|
|
703
|
+
end
|
|
704
|
+
after
|
|
705
|
+
end
|
|
706
|
+
|
|
707
|
+
def drain_kwsh_chunks(chunks)
|
|
503
708
|
loop do
|
|
504
709
|
@prompt.write_transcript_delta(chunks.pop(true))
|
|
505
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,19 +50,23 @@ 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)
|
|
55
57
|
[true, nil]
|
|
56
58
|
when "scratchpad"
|
|
57
|
-
open_scratchpad_command(argument)
|
|
59
|
+
result = open_scratchpad_command(argument)
|
|
60
|
+
queue_editor_prompt_action(result)
|
|
58
61
|
[true, nil]
|
|
59
62
|
when "pty"
|
|
60
63
|
run_interactive_pty_command(argument, agent)
|
|
61
64
|
[true, nil]
|
|
62
65
|
when "tab"
|
|
63
66
|
[true, handle_tab_command(argument, session_store)]
|
|
67
|
+
when "worktree"
|
|
68
|
+
handle_worktree_command(argument)
|
|
69
|
+
[true, active_tab&.agent]
|
|
64
70
|
when "settings"
|
|
65
71
|
configure_settings(agent.conversation)
|
|
66
72
|
[true, nil]
|
|
@@ -300,20 +306,23 @@ module Kward
|
|
|
300
306
|
end
|
|
301
307
|
|
|
302
308
|
def open_scratchpad_command(argument)
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
else
|
|
306
|
-
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.")
|
|
307
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)
|
|
308
319
|
end
|
|
309
320
|
|
|
310
|
-
def
|
|
311
|
-
|
|
312
|
-
return :text if value.empty? || value == "text"
|
|
313
|
-
return :markdown if ["markdown", "md"].include?(value)
|
|
314
|
-
return :ruby if ["ruby", "rb"].include?(value)
|
|
321
|
+
def queue_editor_prompt_action(result)
|
|
322
|
+
return unless result.is_a?(Hash) && result[:editor_prompt]
|
|
315
323
|
|
|
316
|
-
|
|
324
|
+
@pending_inputs ||= []
|
|
325
|
+
@pending_inputs.unshift(result)
|
|
317
326
|
end
|
|
318
327
|
|
|
319
328
|
def open_project_files_browser
|