kward 0.82.0 → 0.83.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 +28 -0
- data/Gemfile.lock +2 -2
- data/doc/agent-tools.md +1 -0
- data/doc/composer.md +1 -1
- data/doc/configuration.md +18 -3
- data/doc/editor.md +20 -10
- data/doc/files.md +4 -3
- data/doc/permissions.md +1 -0
- data/doc/releasing.md +12 -4
- data/doc/rpc.md +2 -2
- data/doc/sandboxing.md +5 -1
- data/doc/security.md +1 -1
- data/doc/shell.md +40 -16
- data/doc/tabs.md +3 -0
- data/doc/usage.md +3 -2
- data/lib/kward/agent.rb +1 -0
- data/lib/kward/auth/anthropic_oauth.rb +7 -7
- data/lib/kward/cli/interactive_turn.rb +101 -19
- data/lib/kward/cli/runtime_helpers.rb +156 -14
- data/lib/kward/cli/slash_commands.rb +12 -1
- data/lib/kward/cli/tabs.rb +12 -5
- data/lib/kward/cli.rb +10 -0
- data/lib/kward/compaction/token_estimator.rb +12 -6
- data/lib/kward/config_files.rb +17 -0
- data/lib/kward/editor_prompt.rb +46 -0
- data/lib/kward/editor_prompt_session.rb +28 -0
- data/lib/kward/ekwsh.rb +70 -10
- data/lib/kward/model/client.rb +1 -17
- data/lib/kward/model/model_info.rb +3 -2
- data/lib/kward/model/payloads.rb +0 -2
- data/lib/kward/persistent_shell_session.rb +750 -0
- data/lib/kward/project_files.rb +18 -5
- data/lib/kward/prompt_interface/composer_renderer.rb +1 -1
- data/lib/kward/prompt_interface/editor/auto_indent.rb +3 -0
- data/lib/kward/prompt_interface/editor/controller.rb +69 -0
- data/lib/kward/prompt_interface/editor/modes/modern.rb +4 -0
- data/lib/kward/prompt_interface/editor/modes/vibe.rb +10 -4
- data/lib/kward/prompt_interface/editor/renderer.rb +1 -0
- data/lib/kward/prompt_interface/editor/state.rb +11 -0
- data/lib/kward/prompt_interface/editor/word_completion.rb +124 -0
- data/lib/kward/prompt_interface/file_overlay.rb +21 -7
- data/lib/kward/prompt_interface/key_handler.rb +8 -0
- data/lib/kward/prompt_interface/project_browser.rb +30 -5
- data/lib/kward/prompt_interface/runtime_state.rb +5 -1
- data/lib/kward/prompt_interface.rb +76 -0
- data/lib/kward/prompts/commands.rb +1 -0
- data/lib/kward/rpc/auth_manager.rb +1 -1
- data/lib/kward/rpc/server.rb +2 -1
- data/lib/kward/shell_prompt.rb +50 -0
- data/lib/kward/shell_prompt_session.rb +58 -0
- data/lib/kward/terminal_keys.rb +1 -0
- data/lib/kward/tools/prepare_shell_command.rb +28 -0
- data/lib/kward/tools/registry.rb +63 -5
- data/lib/kward/tools/replace_editor_buffer.rb +30 -0
- data/lib/kward/tools/run_shell_command.rb +24 -6
- data/lib/kward/version.rb +1 -1
- data/templates/default/layout/html/layout.erb +1 -1
- metadata +9 -1
|
@@ -6,9 +6,68 @@ module Kward
|
|
|
6
6
|
module InteractiveTurn
|
|
7
7
|
private
|
|
8
8
|
|
|
9
|
-
def
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
def run_editor_prompt_turn(action, agent)
|
|
10
|
+
unless agent.respond_to?(:tool_registry) && agent.tool_registry.respond_to?(:for_editor_prompt)
|
|
11
|
+
runtime_output("Editor prompts are unavailable in this tab.")
|
|
12
|
+
return []
|
|
13
|
+
end
|
|
14
|
+
unless @prompt.respond_to?(:editor_prompt_context)
|
|
15
|
+
runtime_output("Editor prompts are unavailable in this prompt.")
|
|
16
|
+
return []
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
context = @prompt.editor_prompt_context
|
|
20
|
+
unless context
|
|
21
|
+
runtime_output("Open a Modern or Vibe editor buffer before prompting the agent.")
|
|
22
|
+
return []
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
unless @prompt.suspend_editor_for_agent
|
|
26
|
+
runtime_output("The editor is busy.")
|
|
27
|
+
return []
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
session = EditorPromptSession.new(context)
|
|
31
|
+
registry = agent.tool_registry.for_editor_prompt(session)
|
|
32
|
+
editor_agent = build_editor_prompt_agent(agent, context, registry)
|
|
33
|
+
instruction = action[:instruction] || action["instruction"]
|
|
34
|
+
run_interactive_turn(
|
|
35
|
+
editor_agent,
|
|
36
|
+
EditorPrompt.input(instruction, context),
|
|
37
|
+
tool_registry: registry,
|
|
38
|
+
editor_prompt_session: session,
|
|
39
|
+
suppress_transcript: true
|
|
40
|
+
)
|
|
41
|
+
rescue StandardError => e
|
|
42
|
+
@prompt.resume_editor_for_agent(status: "Agent request failed") if @prompt.respond_to?(:resume_editor_for_agent)
|
|
43
|
+
runtime_output("Error: #{e.message}")
|
|
44
|
+
[]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def build_editor_prompt_agent(active_agent, context, registry)
|
|
48
|
+
active_conversation = active_agent.conversation
|
|
49
|
+
config = safely_read_config.to_h
|
|
50
|
+
provider = active_conversation.provider || current_model_provider
|
|
51
|
+
model = ConfigFiles.editor_agent_model(config) || active_conversation.model || current_model_id
|
|
52
|
+
reasoning = ConfigFiles.editor_agent_reasoning_effort(config) || active_conversation.reasoning_effort || current_reasoning_effort
|
|
53
|
+
conversation = Conversation.new(
|
|
54
|
+
system_message: EditorPrompt.system_message,
|
|
55
|
+
workspace_root: context[:workspace_root] || active_conversation.workspace_root,
|
|
56
|
+
provider: provider,
|
|
57
|
+
model: model,
|
|
58
|
+
reasoning_effort: reasoning
|
|
59
|
+
)
|
|
60
|
+
Agent.new(
|
|
61
|
+
client: @client,
|
|
62
|
+
tool_registry: registry,
|
|
63
|
+
conversation: conversation,
|
|
64
|
+
warning_sink: ConfigFiles.warning_sink
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def run_interactive_turn(agent, input, display_input: nil, tool_registry: nil, editor_prompt_session: nil, suppress_transcript: false, busy_label: "You>", on_complete: nil)
|
|
69
|
+
prepare_memory_context(agent.conversation, input) if agent.respond_to?(:conversation) && !suppress_transcript
|
|
70
|
+
return run_blocking_interactive_turn(agent, input, display_input: display_input, tool_registry: tool_registry, suppress_transcript: suppress_transcript, on_complete: on_complete) unless prompt_interface?
|
|
12
71
|
|
|
13
72
|
queued_inputs = []
|
|
14
73
|
cancellation = Cancellation.new
|
|
@@ -25,11 +84,11 @@ module Kward
|
|
|
25
84
|
markdown_chunks = []
|
|
26
85
|
answer = nil
|
|
27
86
|
error = nil
|
|
28
|
-
@prompt.begin_busy_input(
|
|
29
|
-
print_user_transcript(input, display_input: display_input)
|
|
87
|
+
@prompt.begin_busy_input(busy_label) if @prompt.respond_to?(:begin_busy_input)
|
|
88
|
+
print_user_transcript(input, display_input: display_input) unless suppress_transcript
|
|
30
89
|
|
|
31
90
|
worker = Thread.new do
|
|
32
|
-
options = agent_display_options(display_input)
|
|
91
|
+
options = agent_display_options(display_input, tool_registry: tool_registry)
|
|
33
92
|
options[:cancellation] = cancellation
|
|
34
93
|
options[:steering] = steering if steering
|
|
35
94
|
answer = agent.ask(input, **options) do |event|
|
|
@@ -52,7 +111,7 @@ module Kward
|
|
|
52
111
|
cancellation.cancel!
|
|
53
112
|
worker.raise(Cancellation::CancelledError, "cancelled") if worker.alive?
|
|
54
113
|
end
|
|
55
|
-
if busy_replacement_agent?
|
|
114
|
+
if busy_replacement_agent? || suppress_transcript
|
|
56
115
|
discard_interactive_events(event_queue, markdown_chunks, stream_state)
|
|
57
116
|
else
|
|
58
117
|
drain_interactive_events(event_queue, markdown_chunks, stream_state, agent)
|
|
@@ -64,19 +123,39 @@ module Kward
|
|
|
64
123
|
error ||= e
|
|
65
124
|
end
|
|
66
125
|
drain_busy_input(queued_inputs, nil) unless cancelled
|
|
67
|
-
if busy_replacement_agent?
|
|
126
|
+
if busy_replacement_agent? || suppress_transcript
|
|
68
127
|
discard_interactive_events(event_queue, markdown_chunks, stream_state, force: true)
|
|
69
128
|
else
|
|
70
129
|
drain_interactive_events(event_queue, markdown_chunks, stream_state, agent, force: true)
|
|
71
130
|
end
|
|
131
|
+
completed = !cancelled && !busy_replacement_agent? && !error
|
|
132
|
+
on_complete&.call(successful: completed, cancelled: cancelled, error: error, answer: answer)
|
|
72
133
|
raise error if error && !error.is_a?(Cancellation::CancelledError) && !busy_replacement_agent?
|
|
73
134
|
|
|
74
|
-
@prompt.
|
|
75
|
-
|
|
76
|
-
|
|
135
|
+
@prompt.commit_editor_prompt(editor_prompt_session) if editor_prompt_session && completed
|
|
136
|
+
@prompt.say("\n#{colored(assistant_output_prompt, :green, :bold)} #{render_markdown_transcript(answer)}\n") unless suppress_transcript || cancelled || busy_replacement_agent? || stream_state[:streamed] || answer.to_s.empty?
|
|
137
|
+
persist_memory_state(agent.conversation) if agent.respond_to?(:conversation) && !suppress_transcript
|
|
138
|
+
auto_summarize_memory(agent.conversation) if agent.respond_to?(:conversation) && !suppress_transcript && queued_inputs.empty? && !cancelled
|
|
77
139
|
queued_inputs
|
|
78
140
|
ensure
|
|
79
141
|
@prompt.finish_busy_input if @prompt.respond_to?(:finish_busy_input)
|
|
142
|
+
if editor_prompt_session && @prompt.respond_to?(:resume_editor_for_agent)
|
|
143
|
+
status = if cancelled
|
|
144
|
+
"Agent request cancelled"
|
|
145
|
+
elsif error
|
|
146
|
+
"Agent request failed"
|
|
147
|
+
elsif editor_prompt_session && !editor_prompt_session.changed? && !answer.to_s.strip.empty?
|
|
148
|
+
editor_prompt_answer_status(answer)
|
|
149
|
+
end
|
|
150
|
+
@prompt.resume_editor_for_agent(status: status)
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def editor_prompt_answer_status(answer)
|
|
155
|
+
text = answer.to_s.gsub(/\s+/, " ").strip
|
|
156
|
+
text = text[0, 120].to_s
|
|
157
|
+
text += "…" if answer.to_s.gsub(/\s+/, " ").strip.length > text.length
|
|
158
|
+
"Agent: #{text}"
|
|
80
159
|
end
|
|
81
160
|
|
|
82
161
|
def drain_interactive_events(event_queue, markdown_chunks, stream_state, agent = nil, force: false)
|
|
@@ -261,21 +340,24 @@ module Kward
|
|
|
261
340
|
ModelInfo.reasoning_supported?(current_model_provider, model)
|
|
262
341
|
end
|
|
263
342
|
|
|
264
|
-
def run_blocking_interactive_turn(agent, input, display_input: nil)
|
|
343
|
+
def run_blocking_interactive_turn(agent, input, display_input: nil, tool_registry: nil, suppress_transcript: false, on_complete: nil)
|
|
265
344
|
streamed = false
|
|
266
345
|
markdown_chunks = []
|
|
267
|
-
answer = agent.ask(input, **agent_display_options(display_input)) do |event|
|
|
268
|
-
streamed = true if render_blocking_turn_event(event, markdown_chunks, tool_line_limit: INTERACTIVE_TOOL_OUTPUT_LINE_LIMIT)
|
|
346
|
+
answer = agent.ask(input, **agent_display_options(display_input, tool_registry: tool_registry)) do |event|
|
|
347
|
+
streamed = true if !suppress_transcript && render_blocking_turn_event(event, markdown_chunks, tool_line_limit: INTERACTIVE_TOOL_OUTPUT_LINE_LIMIT)
|
|
269
348
|
end
|
|
270
349
|
flush_markdown_deltas(markdown_chunks) if streamed
|
|
271
|
-
@prompt.say("\n#{colored(assistant_output_prompt, :green, :bold)} #{render_markdown_transcript(answer)}\n") unless streamed || answer.to_s.empty?
|
|
272
|
-
persist_memory_state(agent.conversation) if agent.respond_to?(:conversation)
|
|
273
|
-
auto_summarize_memory(agent.conversation) if agent.respond_to?(:conversation)
|
|
350
|
+
@prompt.say("\n#{colored(assistant_output_prompt, :green, :bold)} #{render_markdown_transcript(answer)}\n") unless suppress_transcript || streamed || answer.to_s.empty?
|
|
351
|
+
persist_memory_state(agent.conversation) if agent.respond_to?(:conversation) && !suppress_transcript
|
|
352
|
+
auto_summarize_memory(agent.conversation) if agent.respond_to?(:conversation) && !suppress_transcript
|
|
353
|
+
on_complete&.call(successful: true, cancelled: false, error: nil, answer: answer)
|
|
274
354
|
[]
|
|
275
355
|
end
|
|
276
356
|
|
|
277
|
-
def agent_display_options(display_input)
|
|
278
|
-
display_input.nil? ? {} : { display_input: display_input }
|
|
357
|
+
def agent_display_options(display_input, tool_registry: nil)
|
|
358
|
+
options = display_input.nil? ? {} : { display_input: display_input }
|
|
359
|
+
options[:tool_registry] = tool_registry if tool_registry
|
|
360
|
+
options
|
|
279
361
|
end
|
|
280
362
|
|
|
281
363
|
end
|
|
@@ -192,13 +192,86 @@ module Kward
|
|
|
192
192
|
entering = tab.nil? || tab.shell.nil?
|
|
193
193
|
shell = tab&.shell || build_ekwsh(agent)
|
|
194
194
|
tab.shell = shell if tab
|
|
195
|
+
shell_agent = shell_prompt_agent_for(agent, tab: tab)
|
|
195
196
|
runtime_output("Entering ekwsh. Type exit or press Ctrl+D on an empty prompt to return.") if entering
|
|
196
|
-
run_ekwsh_loop(shell, tab: tab, history: build_ekwsh_history(agent))
|
|
197
|
+
run_ekwsh_loop(shell, tab: tab, history: build_ekwsh_history(agent), shell_agent: shell_agent)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def shell_prompt_agent_for(agent, tab: nil)
|
|
201
|
+
return tab.shell_agent if tab&.shell_agent
|
|
202
|
+
return nil unless agent.respond_to?(:conversation) && agent.respond_to?(:tool_registry)
|
|
203
|
+
|
|
204
|
+
shell_agent = build_shell_prompt_agent(agent)
|
|
205
|
+
tab.shell_agent = shell_agent if tab
|
|
206
|
+
shell_agent
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def build_shell_prompt_agent(active_agent)
|
|
210
|
+
active_conversation = active_agent.conversation
|
|
211
|
+
conversation = Conversation.new(
|
|
212
|
+
system_message: ShellPrompt.system_message,
|
|
213
|
+
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
|
|
217
|
+
)
|
|
218
|
+
Agent.new(
|
|
219
|
+
client: @client,
|
|
220
|
+
tool_registry: active_agent.tool_registry,
|
|
221
|
+
conversation: conversation,
|
|
222
|
+
warning_sink: ConfigFiles.warning_sink
|
|
223
|
+
)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def run_shell_prompt_turn(input, shell, shell_agent)
|
|
227
|
+
tab = active_tab if respond_to?(:active_tab, true)
|
|
228
|
+
transcript_before = prompt_transcript_text
|
|
229
|
+
instruction = input.to_s.sub(/\A\?\s*/, "").strip
|
|
230
|
+
if instruction.empty?
|
|
231
|
+
runtime_output("Shell prompt instruction required after ?")
|
|
232
|
+
return []
|
|
233
|
+
end
|
|
234
|
+
unless shell_agent.respond_to?(:tool_registry)
|
|
235
|
+
runtime_output("Shell agent is unavailable in this prompt.")
|
|
236
|
+
return []
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
session = ShellPromptSession.new(shell)
|
|
240
|
+
registry = shell_agent.tool_registry.for_shell_prompt(session)
|
|
241
|
+
on_complete = lambda do |successful:, **|
|
|
242
|
+
next unless successful
|
|
243
|
+
next unless session.prepared_command
|
|
244
|
+
|
|
245
|
+
if @prompt.respond_to?(:prefill_input)
|
|
246
|
+
@prompt.prefill_input(session.prepared_command)
|
|
247
|
+
else
|
|
248
|
+
runtime_output("Prepared shell command:\n#{session.prepared_command}")
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
pending_inputs = run_interactive_turn(
|
|
252
|
+
shell_agent,
|
|
253
|
+
ShellPrompt.input(instruction, session.context),
|
|
254
|
+
display_input: "? #{instruction}",
|
|
255
|
+
tool_registry: registry,
|
|
256
|
+
busy_label: shell.prompt_label,
|
|
257
|
+
on_complete: on_complete
|
|
258
|
+
)
|
|
259
|
+
@prompt.update_prompt_label(shell.prompt_label) if @prompt.respond_to?(:update_prompt_label)
|
|
260
|
+
pending_inputs
|
|
261
|
+
rescue StandardError => e
|
|
262
|
+
runtime_output("Error: #{e.message}")
|
|
263
|
+
[]
|
|
264
|
+
ensure
|
|
265
|
+
record_shell_prompt_transcript(tab, transcript_before)
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def shell_prompt_input?(input)
|
|
269
|
+
input.to_s.start_with?("?")
|
|
197
270
|
end
|
|
198
271
|
|
|
199
272
|
def build_ekwsh(agent)
|
|
200
273
|
config = ConfigFiles.read_ekwsh_config
|
|
201
|
-
|
|
274
|
+
PersistentShellSession.new(
|
|
202
275
|
cwd: interactive_workspace_root(agent),
|
|
203
276
|
configured_env: config[:env],
|
|
204
277
|
aliases: config[:aliases],
|
|
@@ -242,7 +315,8 @@ module Kward
|
|
|
242
315
|
@prompt.say(intro_message)
|
|
243
316
|
end
|
|
244
317
|
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)
|
|
318
|
+
transcript_output = record_completed_pty_output(sink, completed_result)
|
|
319
|
+
yield(transcript_output, completed_result) if block_given?
|
|
246
320
|
end
|
|
247
321
|
refresh_composer_status
|
|
248
322
|
result
|
|
@@ -253,12 +327,18 @@ module Kward
|
|
|
253
327
|
|
|
254
328
|
def run_interactive_pty_with_terminal_handoff(shell, command, env:, cwd:, &on_complete)
|
|
255
329
|
runner = InteractivePtyRunner.new
|
|
256
|
-
|
|
257
|
-
result =
|
|
330
|
+
with_interactive_terminal_handoff do |input, sink|
|
|
331
|
+
result = if shell.respond_to?(:run_interactive)
|
|
332
|
+
shell.run_interactive(command, input: input, sink: sink)
|
|
333
|
+
else
|
|
334
|
+
runner.run(shell, "-c", command, env: env, cwd: cwd, input: input, sink: sink)
|
|
335
|
+
end
|
|
258
336
|
on_complete.call(sink, result) if on_complete
|
|
259
337
|
result
|
|
260
338
|
end
|
|
339
|
+
end
|
|
261
340
|
|
|
341
|
+
def with_interactive_terminal_handoff
|
|
262
342
|
if @prompt.respond_to?(:with_inline_terminal_handoff)
|
|
263
343
|
@prompt.with_inline_terminal_handoff do |input, output, transition|
|
|
264
344
|
sink = AdaptivePtyOutputSink.new(
|
|
@@ -266,7 +346,7 @@ module Kward
|
|
|
266
346
|
on_exclusive: transition,
|
|
267
347
|
max_capture_bytes: MAX_TRANSIENT_TERMINAL_OUTPUT_BYTES
|
|
268
348
|
)
|
|
269
|
-
|
|
349
|
+
yield(input, sink)
|
|
270
350
|
end
|
|
271
351
|
elsif @prompt.respond_to?(:with_terminal_handoff)
|
|
272
352
|
@prompt.with_terminal_handoff do |input, output|
|
|
@@ -274,14 +354,14 @@ module Kward
|
|
|
274
354
|
output: output,
|
|
275
355
|
max_capture_bytes: MAX_TRANSIENT_TERMINAL_OUTPUT_BYTES
|
|
276
356
|
)
|
|
277
|
-
|
|
357
|
+
yield(input, sink)
|
|
278
358
|
end
|
|
279
359
|
else
|
|
280
360
|
sink = PassthroughPtyOutputSink.new(
|
|
281
361
|
output: $stdout,
|
|
282
362
|
max_capture_bytes: MAX_TRANSIENT_TERMINAL_OUTPUT_BYTES
|
|
283
363
|
)
|
|
284
|
-
|
|
364
|
+
yield($stdin, sink)
|
|
285
365
|
end
|
|
286
366
|
end
|
|
287
367
|
|
|
@@ -306,6 +386,7 @@ module Kward
|
|
|
306
386
|
if @prompt.respond_to?(:record_transient_terminal_output)
|
|
307
387
|
@prompt.record_transient_terminal_output(transcript_output, render: false)
|
|
308
388
|
end
|
|
389
|
+
transcript_output
|
|
309
390
|
end
|
|
310
391
|
|
|
311
392
|
def terminal_transcript_output(
|
|
@@ -341,13 +422,15 @@ module Kward
|
|
|
341
422
|
end
|
|
342
423
|
end
|
|
343
424
|
|
|
344
|
-
def run_ekwsh_loop(shell, tab: nil, history: nil)
|
|
425
|
+
def run_ekwsh_loop(shell, tab: nil, history: nil, shell_agent: nil)
|
|
426
|
+
shell_agent ||= shell_prompt_agent_for(tab.agent, tab: tab) if tab&.agent
|
|
345
427
|
with_ekwsh_history(history) do
|
|
346
|
-
run_ekwsh_loop_with_history(shell, tab: tab)
|
|
428
|
+
run_ekwsh_loop_with_history(shell, tab: tab, shell_agent: shell_agent)
|
|
347
429
|
end
|
|
348
430
|
end
|
|
349
431
|
|
|
350
|
-
def run_ekwsh_loop_with_history(shell, tab: nil)
|
|
432
|
+
def run_ekwsh_loop_with_history(shell, tab: nil, shell_agent: nil)
|
|
433
|
+
pending_shell_inputs = []
|
|
351
434
|
loop do
|
|
352
435
|
if @prompt.respond_to?(:editing_file?) && @prompt.editing_file?
|
|
353
436
|
editor_result = @prompt.run_editor
|
|
@@ -357,13 +440,18 @@ module Kward
|
|
|
357
440
|
end
|
|
358
441
|
end
|
|
359
442
|
|
|
360
|
-
input = ask_ekwsh(shell)
|
|
443
|
+
input = pending_shell_inputs.shift || ask_ekwsh(shell)
|
|
361
444
|
if input.is_a?(Hash) && input[:tab_action]
|
|
362
445
|
(@pending_inputs ||= []).unshift(input)
|
|
363
446
|
return :tab_action
|
|
364
447
|
end
|
|
365
448
|
break if input.nil?
|
|
366
449
|
|
|
450
|
+
if shell_prompt_input?(input)
|
|
451
|
+
pending_shell_inputs.concat(run_shell_prompt_turn(input, shell, shell_agent))
|
|
452
|
+
next
|
|
453
|
+
end
|
|
454
|
+
|
|
367
455
|
result = run_ekwsh_command(shell, input)
|
|
368
456
|
if result.clear
|
|
369
457
|
clear_active_tab_transient_shell_output
|
|
@@ -385,16 +473,26 @@ module Kward
|
|
|
385
473
|
next
|
|
386
474
|
end
|
|
387
475
|
if result.exit_shell
|
|
476
|
+
close_ekwsh_session(shell)
|
|
388
477
|
tab.shell = nil if tab
|
|
478
|
+
tab.shell_agent = nil if tab
|
|
389
479
|
runtime_output("Shell exited.")
|
|
390
480
|
return :exited
|
|
391
481
|
end
|
|
392
482
|
end
|
|
483
|
+
close_ekwsh_session(shell)
|
|
393
484
|
tab.shell = nil if tab
|
|
485
|
+
tab.shell_agent = nil if tab
|
|
394
486
|
runtime_output("Shell exited.")
|
|
395
487
|
:exited
|
|
396
488
|
end
|
|
397
489
|
|
|
490
|
+
def close_ekwsh_session(shell)
|
|
491
|
+
shell.close if shell.respond_to?(:close)
|
|
492
|
+
rescue StandardError
|
|
493
|
+
nil
|
|
494
|
+
end
|
|
495
|
+
|
|
398
496
|
def with_ekwsh_history(history)
|
|
399
497
|
if history && @prompt.respond_to?(:with_prompt_history)
|
|
400
498
|
@prompt.with_prompt_history(history) { yield }
|
|
@@ -428,13 +526,19 @@ module Kward
|
|
|
428
526
|
end
|
|
429
527
|
|
|
430
528
|
def run_ekwsh_interactive_pty_command(shell, result)
|
|
529
|
+
shell_target = shell.respond_to?(:run_interactive) ? shell : shell.command_shell
|
|
431
530
|
run_user_interactive_pty_command(
|
|
432
531
|
result.interactive_command,
|
|
433
|
-
shell:
|
|
532
|
+
shell: shell_target,
|
|
434
533
|
env: shell.child_env(interactive: true),
|
|
435
534
|
cwd: shell.cwd,
|
|
436
535
|
intro: result.output
|
|
437
|
-
)
|
|
536
|
+
) do |transcript_output, completed_result|
|
|
537
|
+
shell.record_interactive_result(
|
|
538
|
+
output: transcript_output,
|
|
539
|
+
exit_status: completed_result.exit_status
|
|
540
|
+
)
|
|
541
|
+
end
|
|
438
542
|
end
|
|
439
543
|
|
|
440
544
|
def run_ekwsh_command(shell, input)
|
|
@@ -499,6 +603,44 @@ module Kward
|
|
|
499
603
|
clear_active_tab_transient_shell_entries if respond_to?(:clear_active_tab_transient_shell_entries, true)
|
|
500
604
|
end
|
|
501
605
|
|
|
606
|
+
def prompt_transcript_text
|
|
607
|
+
return nil unless @prompt.respond_to?(:tab_view_snapshot)
|
|
608
|
+
|
|
609
|
+
snapshot = @prompt.tab_view_snapshot
|
|
610
|
+
transcript = snapshot[:transcript_buffer] || snapshot["transcript_buffer"] || snapshot[:transcript] || snapshot["transcript"]
|
|
611
|
+
transcript = transcript.map(&:to_s).join if transcript.is_a?(Array)
|
|
612
|
+
transcript.to_s
|
|
613
|
+
rescue StandardError
|
|
614
|
+
nil
|
|
615
|
+
end
|
|
616
|
+
|
|
617
|
+
def record_shell_prompt_transcript(tab, transcript_before)
|
|
618
|
+
return unless tab && !transcript_before.nil?
|
|
619
|
+
|
|
620
|
+
transcript_after = prompt_transcript_text
|
|
621
|
+
return if transcript_after.nil?
|
|
622
|
+
|
|
623
|
+
delta = shell_prompt_transcript_delta(transcript_before, transcript_after)
|
|
624
|
+
tab.append_transient_shell_entry(delta) unless delta.empty?
|
|
625
|
+
rescue StandardError
|
|
626
|
+
nil
|
|
627
|
+
end
|
|
628
|
+
|
|
629
|
+
def shell_prompt_transcript_delta(before, after)
|
|
630
|
+
return "" if before == after
|
|
631
|
+
return after[before.length..].to_s if after.start_with?(before)
|
|
632
|
+
|
|
633
|
+
# The prompt transcript is bounded and may trim while a large agent
|
|
634
|
+
# response is rendered. Preserve only the part not already retained.
|
|
635
|
+
overlap = [before.length, after.length].min
|
|
636
|
+
while overlap.positive?
|
|
637
|
+
return after[overlap..].to_s if before.end_with?(after[0, overlap])
|
|
638
|
+
|
|
639
|
+
overlap -= 1
|
|
640
|
+
end
|
|
641
|
+
after
|
|
642
|
+
end
|
|
643
|
+
|
|
502
644
|
def drain_ekwsh_chunks(chunks)
|
|
503
645
|
loop do
|
|
504
646
|
@prompt.write_transcript_delta(chunks.pop(true))
|
|
@@ -54,13 +54,17 @@ module Kward
|
|
|
54
54
|
run_captured_shell_command(argument, agent)
|
|
55
55
|
[true, nil]
|
|
56
56
|
when "scratchpad"
|
|
57
|
-
open_scratchpad_command(argument)
|
|
57
|
+
result = open_scratchpad_command(argument)
|
|
58
|
+
queue_editor_prompt_action(result)
|
|
58
59
|
[true, nil]
|
|
59
60
|
when "pty"
|
|
60
61
|
run_interactive_pty_command(argument, agent)
|
|
61
62
|
[true, nil]
|
|
62
63
|
when "tab"
|
|
63
64
|
[true, handle_tab_command(argument, session_store)]
|
|
65
|
+
when "worktree"
|
|
66
|
+
handle_worktree_command(argument)
|
|
67
|
+
[true, active_tab&.agent]
|
|
64
68
|
when "settings"
|
|
65
69
|
configure_settings(agent.conversation)
|
|
66
70
|
[true, nil]
|
|
@@ -307,6 +311,13 @@ module Kward
|
|
|
307
311
|
end
|
|
308
312
|
end
|
|
309
313
|
|
|
314
|
+
def queue_editor_prompt_action(result)
|
|
315
|
+
return unless result.is_a?(Hash) && result[:editor_prompt]
|
|
316
|
+
|
|
317
|
+
@pending_inputs ||= []
|
|
318
|
+
@pending_inputs.unshift(result)
|
|
319
|
+
end
|
|
320
|
+
|
|
310
321
|
def scratchpad_language_argument(argument)
|
|
311
322
|
value = argument.to_s.strip.downcase
|
|
312
323
|
return :text if value.empty? || value == "text"
|
data/lib/kward/cli/tabs.rb
CHANGED
|
@@ -35,6 +35,7 @@ module Kward
|
|
|
35
35
|
:unread,
|
|
36
36
|
:pending_question,
|
|
37
37
|
:shell,
|
|
38
|
+
:shell_agent,
|
|
38
39
|
:transient_shell_entries,
|
|
39
40
|
:error_reported,
|
|
40
41
|
:local_busy_activity,
|
|
@@ -317,6 +318,7 @@ module Kward
|
|
|
317
318
|
unread: false,
|
|
318
319
|
pending_question: nil,
|
|
319
320
|
shell: nil,
|
|
321
|
+
shell_agent: nil,
|
|
320
322
|
transient_shell_entries: [],
|
|
321
323
|
error_reported: false,
|
|
322
324
|
local_busy_activity: nil
|
|
@@ -418,12 +420,14 @@ module Kward
|
|
|
418
420
|
end
|
|
419
421
|
|
|
420
422
|
if @tabs.length <= 1
|
|
423
|
+
close_ekwsh_session(tab.shell) if tab&.shell && respond_to?(:close_ekwsh_session, true)
|
|
421
424
|
@tabs.clear
|
|
422
425
|
persist_tabs
|
|
423
426
|
return PromptInterface::EXIT_INPUT
|
|
424
427
|
end
|
|
425
428
|
|
|
426
429
|
stop_tab_live_view
|
|
430
|
+
close_ekwsh_session(tab.shell) if tab&.shell && respond_to?(:close_ekwsh_session, true)
|
|
427
431
|
tab.session&.delete_if_unused if tab&.session.respond_to?(:delete_if_unused)
|
|
428
432
|
tab.driver.close if tab.driver.respond_to?(:close)
|
|
429
433
|
@tabs.delete_at(@active_tab_index)
|
|
@@ -462,7 +466,9 @@ module Kward
|
|
|
462
466
|
tab.seen_events = 0
|
|
463
467
|
tab.queued_inputs.clear
|
|
464
468
|
tab.steering = nil
|
|
469
|
+
close_ekwsh_session(tab.shell) if tab.shell && respond_to?(:close_ekwsh_session, true)
|
|
465
470
|
tab.shell = nil
|
|
471
|
+
tab.shell_agent = nil
|
|
466
472
|
tab.clear_transient_shell_entries
|
|
467
473
|
tab.stream_state = new_tab_stream_state(tab.driver)
|
|
468
474
|
tab.markdown_chunks.clear
|
|
@@ -959,11 +965,12 @@ module Kward
|
|
|
959
965
|
def stop_tabs
|
|
960
966
|
stop_tab_live_view
|
|
961
967
|
Array(@tabs).each do |tab|
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
968
|
+
if tab&.running?
|
|
969
|
+
tab.cancellation&.cancel!
|
|
970
|
+
tab.thread&.raise(Cancellation::CancelledError, "cancelled") if tab.thread&.alive?
|
|
971
|
+
tab.thread&.join(0.2)
|
|
972
|
+
end
|
|
973
|
+
close_ekwsh_session(tab.shell) if tab&.shell && respond_to?(:close_ekwsh_session, true)
|
|
967
974
|
rescue StandardError
|
|
968
975
|
nil
|
|
969
976
|
end
|
data/lib/kward/cli.rb
CHANGED
|
@@ -10,11 +10,16 @@ require_relative "compactor"
|
|
|
10
10
|
require_relative "config_files"
|
|
11
11
|
require_relative "clipboard"
|
|
12
12
|
require_relative "cancellation"
|
|
13
|
+
require_relative "editor_prompt"
|
|
14
|
+
require_relative "editor_prompt_session"
|
|
15
|
+
require_relative "shell_prompt"
|
|
16
|
+
require_relative "shell_prompt_session"
|
|
13
17
|
require_relative "cli_transcript_formatter"
|
|
14
18
|
require_relative "model/context_usage"
|
|
15
19
|
require_relative "events"
|
|
16
20
|
require_relative "export_path"
|
|
17
21
|
require_relative "ekwsh"
|
|
22
|
+
require_relative "persistent_shell_session"
|
|
18
23
|
require_relative "interactive_pty_runner"
|
|
19
24
|
require_relative "adaptive_pty_output_sink"
|
|
20
25
|
require_relative "pty_transcript_normalizer"
|
|
@@ -477,6 +482,11 @@ module Kward
|
|
|
477
482
|
run_ekwsh_loop(active_tab.shell, tab: active_tab, history: build_ekwsh_history(active_tab.agent))
|
|
478
483
|
end
|
|
479
484
|
input = @pending_inputs.shift || (active_tab ? poll_active_tab_input : @prompt.ask("You>"))
|
|
485
|
+
if input.is_a?(Hash) && input[:editor_prompt]
|
|
486
|
+
pending_inputs = run_editor_prompt_turn(input[:editor_prompt], active_tab&.agent || agent)
|
|
487
|
+
pending_inputs.reverse_each { |pending_input| @pending_inputs.unshift(pending_input) }
|
|
488
|
+
next
|
|
489
|
+
end
|
|
480
490
|
if input.is_a?(Hash) && input[:tab_action]
|
|
481
491
|
tab_result = handle_tab_action(input, session_store)
|
|
482
492
|
break if tab_result == PromptInterface::EXIT_INPUT
|
|
@@ -31,13 +31,15 @@ module Kward
|
|
|
31
31
|
parts = [role]
|
|
32
32
|
if role.to_s == "compactionSummary"
|
|
33
33
|
parts << value(message, :summary)
|
|
34
|
-
|
|
34
|
+
elsif response_items(message).empty?
|
|
35
35
|
parts << content_text(value(message, :content))
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
parts << value(message, :reasoning_summary)
|
|
37
|
+
tool_calls(message).each do |tool_call|
|
|
38
|
+
parts << tool_call_name(tool_call)
|
|
39
|
+
parts << tool_call_arguments(tool_call)
|
|
40
|
+
end
|
|
41
|
+
else
|
|
42
|
+
parts << JSON.generate(response_items(message))
|
|
41
43
|
end
|
|
42
44
|
parts << value(message, :tool_call_id)
|
|
43
45
|
parts << value(message, :name)
|
|
@@ -65,6 +67,10 @@ module Kward
|
|
|
65
67
|
MessageAccess.tool_calls(message)
|
|
66
68
|
end
|
|
67
69
|
|
|
70
|
+
def response_items(message)
|
|
71
|
+
MessageAccess.response_items(message)
|
|
72
|
+
end
|
|
73
|
+
|
|
68
74
|
def tool_call_name(tool_call)
|
|
69
75
|
ToolCall.name(tool_call)
|
|
70
76
|
end
|
data/lib/kward/config_files.rb
CHANGED
|
@@ -136,6 +136,7 @@ module Kward
|
|
|
136
136
|
},
|
|
137
137
|
"editor" => {
|
|
138
138
|
"mode" => "modern",
|
|
139
|
+
"agent" => {},
|
|
139
140
|
"auto_indent" => true,
|
|
140
141
|
"auto_close_pairs" => true,
|
|
141
142
|
"soft_wrap" => true,
|
|
@@ -482,6 +483,22 @@ module Kward
|
|
|
482
483
|
EditorMode.normalize(editor["mode"])
|
|
483
484
|
end
|
|
484
485
|
|
|
486
|
+
# Returns the optional model override for editor-agent turns.
|
|
487
|
+
def editor_agent_model(config = read_config)
|
|
488
|
+
editor = config["editor"].is_a?(Hash) ? config["editor"] : {}
|
|
489
|
+
agent = editor["agent"].is_a?(Hash) ? editor["agent"] : {}
|
|
490
|
+
value = agent["model"].to_s.strip
|
|
491
|
+
value.empty? ? nil : value
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
# Returns the optional reasoning-effort override for editor-agent turns.
|
|
495
|
+
def editor_agent_reasoning_effort(config = read_config)
|
|
496
|
+
editor = config["editor"].is_a?(Hash) ? config["editor"] : {}
|
|
497
|
+
agent = editor["agent"].is_a?(Hash) ? editor["agent"] : {}
|
|
498
|
+
value = agent["reasoning_effort"].to_s.strip
|
|
499
|
+
value.empty? ? nil : value
|
|
500
|
+
end
|
|
501
|
+
|
|
485
502
|
# Returns whether the built-in TUI editor should auto-indent new lines.
|
|
486
503
|
def editor_auto_indent?(config = read_config)
|
|
487
504
|
editor = config["editor"].is_a?(Hash) ? config["editor"] : {}
|