kward 0.72.0 → 0.73.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/.github/workflows/ci.yml +30 -0
- data/CHANGELOG.md +53 -0
- data/Gemfile.lock +2 -2
- data/doc/configuration.md +1 -1
- data/doc/editor.md +23 -2
- data/doc/git.md +1 -0
- data/doc/rpc.md +2 -2
- data/doc/shell.md +56 -10
- data/doc/usage.md +27 -1
- data/lib/kward/ansi.rb +62 -23
- data/lib/kward/cli/plugins.rb +1 -1
- data/lib/kward/cli/rendering.rb +4 -1
- data/lib/kward/cli/runtime_helpers.rb +141 -7
- data/lib/kward/cli/settings.rb +0 -1
- data/lib/kward/cli/slash_commands.rb +213 -0
- data/lib/kward/cli/tabs.rb +34 -4
- data/lib/kward/cli/tool_summaries.rb +6 -0
- data/lib/kward/cli.rb +4 -12
- data/lib/kward/clipboard.rb +2 -3
- data/lib/kward/compactor.rb +7 -19
- data/lib/kward/config_files.rb +26 -4
- data/lib/kward/ekwsh.rb +239 -42
- data/lib/kward/image_attachments.rb +3 -1
- data/lib/kward/interactive_pty_runner.rb +151 -0
- data/lib/kward/local_command_runner.rb +155 -0
- data/lib/kward/local_pty_command_runner.rb +171 -0
- data/lib/kward/model/context_usage.rb +2 -2
- data/lib/kward/model/payloads.rb +2 -5
- data/lib/kward/prompt_history.rb +5 -3
- data/lib/kward/prompt_interface/editor/auto_indent.rb +5 -4
- data/lib/kward/prompt_interface/editor/controller.rb +262 -62
- data/lib/kward/prompt_interface/editor/modes/emacs.rb +21 -21
- data/lib/kward/prompt_interface/editor/modes/modern.rb +38 -37
- data/lib/kward/prompt_interface/editor/modes/vibe.rb +23 -173
- data/lib/kward/prompt_interface/editor/modes/vibe_insert_readline.rb +166 -0
- data/lib/kward/prompt_interface/editor/renderer.rb +6 -5
- data/lib/kward/prompt_interface/editor/state.rb +28 -6
- data/lib/kward/prompt_interface/editor/syntax_highlighter.rb +5 -3
- data/lib/kward/prompt_interface/git_prompt.rb +12 -23
- data/lib/kward/prompt_interface/interactive/controller.rb +1 -1
- data/lib/kward/prompt_interface/key_handler.rb +93 -51
- data/lib/kward/prompt_interface/question_prompt.rb +1 -6
- data/lib/kward/prompt_interface/screen.rb +3 -3
- data/lib/kward/prompt_interface/selection_prompt.rb +3 -6
- data/lib/kward/prompt_interface/slash_overlay.rb +2 -0
- data/lib/kward/prompt_interface.rb +87 -221
- data/lib/kward/prompts/commands.rb +4 -0
- data/lib/kward/rpc/memory_methods.rb +83 -0
- data/lib/kward/rpc/server.rb +130 -83
- data/lib/kward/rpc/session_manager.rb +10 -74
- data/lib/kward/rpc/tool_metadata.rb +11 -0
- data/lib/kward/rpc/transcript_normalizer.rb +4 -39
- data/lib/kward/scratchpad_runner.rb +56 -0
- data/lib/kward/session_diff.rb +20 -3
- data/lib/kward/session_naming.rb +11 -0
- data/lib/kward/terminal_keys.rb +84 -0
- data/lib/kward/terminal_sequences.rb +42 -0
- data/lib/kward/tools/context_for_task.rb +2 -0
- data/lib/kward/version.rb +1 -1
- data/lib/kward/workers/git_guard.rb +25 -0
- data/lib/kward/workers/job.rb +99 -0
- data/lib/kward/workers/queue_runner.rb +166 -0
- data/lib/kward/workers/queue_store.rb +112 -0
- data/lib/kward/workers.rb +3 -0
- data/lib/kward/workspace.rb +15 -63
- data/templates/default/fulldoc/html/css/kward.css +33 -0
- data/templates/default/fulldoc/html/images/kward_screen_1.png +0 -0
- data/templates/default/fulldoc/html/setup.rb +1 -0
- data/templates/default/layout/html/layout.erb +19 -32
- metadata +15 -1
|
@@ -125,15 +125,72 @@ module Kward
|
|
|
125
125
|
shell = tab&.shell || build_ekwsh(agent)
|
|
126
126
|
tab.shell = shell if tab
|
|
127
127
|
runtime_output("Entering ekwsh. Type exit or press Ctrl+D on an empty prompt to return.") if entering
|
|
128
|
-
run_ekwsh_loop(shell, tab: tab)
|
|
128
|
+
run_ekwsh_loop(shell, tab: tab, history: build_ekwsh_history(agent))
|
|
129
129
|
end
|
|
130
130
|
|
|
131
131
|
def build_ekwsh(agent)
|
|
132
132
|
config = ConfigFiles.read_ekwsh_config
|
|
133
|
-
Ekwsh.new(
|
|
133
|
+
Ekwsh.new(
|
|
134
|
+
cwd: interactive_workspace_root(agent),
|
|
135
|
+
configured_env: config[:env],
|
|
136
|
+
aliases: config[:aliases],
|
|
137
|
+
shell: config[:shell],
|
|
138
|
+
timeout_seconds: config[:timeout_seconds],
|
|
139
|
+
max_output_bytes: config[:max_output_bytes]
|
|
140
|
+
)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def build_ekwsh_history(agent)
|
|
144
|
+
config = ConfigFiles.read_ekwsh_config
|
|
145
|
+
PromptHistory.new(
|
|
146
|
+
cwd: interactive_workspace_root(agent),
|
|
147
|
+
limit: config[:history_limit],
|
|
148
|
+
kind: "shell"
|
|
149
|
+
)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def run_interactive_pty_command(command, agent)
|
|
153
|
+
command = command.to_s.strip
|
|
154
|
+
if command.empty?
|
|
155
|
+
runtime_output("Usage: /pty <command>")
|
|
156
|
+
return
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
config = ConfigFiles.read_ekwsh_config
|
|
160
|
+
env = interactive_pty_environment(config[:env])
|
|
161
|
+
cwd = interactive_workspace_root(agent)
|
|
162
|
+
@prompt.say("$ #{command}\n[interactive PTY session started]\n") if @prompt.respond_to?(:say)
|
|
163
|
+
result = run_interactive_pty_with_terminal_handoff(config[:shell], command, env: env, cwd: cwd)
|
|
164
|
+
@prompt.say("[interactive PTY session exited with status #{result.exit_status}]\n") if @prompt.respond_to?(:say)
|
|
165
|
+
rescue Errno::ENOENT => e
|
|
166
|
+
runtime_output("Error: #{e.message}")
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def run_interactive_pty_with_terminal_handoff(shell, command, env:, cwd:)
|
|
170
|
+
runner = InteractivePtyRunner.new
|
|
171
|
+
if @prompt.respond_to?(:with_terminal_handoff)
|
|
172
|
+
@prompt.with_terminal_handoff do |input, output|
|
|
173
|
+
runner.run(shell, "-c", command, env: env, cwd: cwd, input: input, output: output)
|
|
174
|
+
end
|
|
175
|
+
else
|
|
176
|
+
runner.run(shell, "-c", command, env: env, cwd: cwd)
|
|
177
|
+
end
|
|
134
178
|
end
|
|
135
179
|
|
|
136
|
-
def
|
|
180
|
+
def interactive_pty_environment(configured_env)
|
|
181
|
+
ENV.to_h.merge(configured_env.to_h.transform_keys(&:to_s).transform_values(&:to_s)).tap do |env|
|
|
182
|
+
env.delete("GIT_PAGER") if env["GIT_PAGER"] == "cat"
|
|
183
|
+
env["TERM"] = "xterm-256color" if env["TERM"].to_s.empty? || env["TERM"] == "dumb"
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def run_ekwsh_loop(shell, tab: nil, history: nil)
|
|
188
|
+
with_ekwsh_history(history) do
|
|
189
|
+
run_ekwsh_loop_with_history(shell, tab: tab)
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def run_ekwsh_loop_with_history(shell, tab: nil)
|
|
137
194
|
loop do
|
|
138
195
|
if @prompt.respond_to?(:editing_file?) && @prompt.editing_file?
|
|
139
196
|
editor_result = @prompt.run_editor
|
|
@@ -152,13 +209,19 @@ module Kward
|
|
|
152
209
|
|
|
153
210
|
result = run_ekwsh_command(shell, input)
|
|
154
211
|
@prompt.clear_transcript if result.clear && @prompt.respond_to?(:clear_transcript)
|
|
155
|
-
@prompt.say(result.output) unless result.output.to_s.empty?
|
|
212
|
+
@prompt.say(result.output) unless result.streamed || result.interactive_command || result.output.to_s.empty?
|
|
213
|
+
return :tab_action if pending_tab_action?
|
|
214
|
+
|
|
156
215
|
if result.open_editor_path
|
|
157
216
|
editor_result = open_ekwsh_editor(result.open_editor_path, shell)
|
|
158
217
|
return :tab_action if editor_result == :tab_action
|
|
159
218
|
|
|
160
219
|
next
|
|
161
220
|
end
|
|
221
|
+
if result.interactive_command
|
|
222
|
+
run_ekwsh_interactive_pty_command(shell, result)
|
|
223
|
+
next
|
|
224
|
+
end
|
|
162
225
|
if result.exit_shell
|
|
163
226
|
tab.shell = nil if tab
|
|
164
227
|
runtime_output("Shell exited.")
|
|
@@ -170,6 +233,14 @@ module Kward
|
|
|
170
233
|
:exited
|
|
171
234
|
end
|
|
172
235
|
|
|
236
|
+
def with_ekwsh_history(history)
|
|
237
|
+
if history && @prompt.respond_to?(:with_prompt_history)
|
|
238
|
+
@prompt.with_prompt_history(history) { yield }
|
|
239
|
+
else
|
|
240
|
+
yield
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
173
244
|
def open_ekwsh_editor(path, shell)
|
|
174
245
|
unless @prompt.respond_to?(:edit_file)
|
|
175
246
|
runtime_output("Integrated editor is unavailable in this prompt.")
|
|
@@ -188,21 +259,82 @@ module Kward
|
|
|
188
259
|
def ask_ekwsh(shell)
|
|
189
260
|
provider = ->(input, cursor) { shell.complete(input, cursor) }
|
|
190
261
|
if @prompt.respond_to?(:with_completion_provider)
|
|
191
|
-
@prompt.with_completion_provider(provider) { @prompt.ask(shell.prompt_label) }
|
|
262
|
+
@prompt.with_completion_provider(provider, slash_overlay: false) { @prompt.ask(shell.prompt_label) }
|
|
192
263
|
else
|
|
193
264
|
@prompt.ask(shell.prompt_label)
|
|
194
265
|
end
|
|
195
266
|
end
|
|
196
267
|
|
|
268
|
+
def run_ekwsh_interactive_pty_command(shell, result)
|
|
269
|
+
@prompt.say(result.output) unless result.output.to_s.empty?
|
|
270
|
+
pty_result = run_interactive_pty_with_terminal_handoff(
|
|
271
|
+
shell.command_shell,
|
|
272
|
+
result.interactive_command,
|
|
273
|
+
env: shell.child_env(interactive: true),
|
|
274
|
+
cwd: shell.cwd
|
|
275
|
+
)
|
|
276
|
+
@prompt.say("[interactive PTY session exited with status #{pty_result.exit_status}]\n") if @prompt.respond_to?(:say)
|
|
277
|
+
end
|
|
278
|
+
|
|
197
279
|
def run_ekwsh_command(shell, input)
|
|
198
280
|
if @prompt.respond_to?(:begin_busy_input)
|
|
199
281
|
@prompt.begin_busy_input(shell.prompt_label, activity: "running")
|
|
200
282
|
end
|
|
201
|
-
|
|
283
|
+
if @prompt.respond_to?(:write_transcript_delta) && @prompt.respond_to?(:poll_input)
|
|
284
|
+
run_streaming_ekwsh_command(shell, input)
|
|
285
|
+
elsif @prompt.respond_to?(:write_transcript_delta)
|
|
286
|
+
shell.run(input) { |chunk| @prompt.write_transcript_delta(chunk) }
|
|
287
|
+
else
|
|
288
|
+
shell.run(input)
|
|
289
|
+
end
|
|
202
290
|
ensure
|
|
203
291
|
@prompt.finish_busy_input if @prompt.respond_to?(:finish_busy_input)
|
|
204
292
|
end
|
|
205
293
|
|
|
294
|
+
def run_streaming_ekwsh_command(shell, input)
|
|
295
|
+
cancellation = Cancellation.new
|
|
296
|
+
chunks = Queue.new
|
|
297
|
+
queued_inputs = []
|
|
298
|
+
result = nil
|
|
299
|
+
error = nil
|
|
300
|
+
worker = Thread.new do
|
|
301
|
+
result = shell.run(input, cancellation: cancellation) { |chunk| chunks << chunk }
|
|
302
|
+
rescue StandardError => e
|
|
303
|
+
error = e
|
|
304
|
+
end
|
|
305
|
+
worker.report_on_exception = false
|
|
306
|
+
|
|
307
|
+
while worker.alive?
|
|
308
|
+
drain_ekwsh_chunks(chunks)
|
|
309
|
+
poll_result = collect_queued_input(queued_inputs)
|
|
310
|
+
if poll_result == PromptInterface::CANCEL_INPUT
|
|
311
|
+
cancellation.cancel!
|
|
312
|
+
elsif poll_result.is_a?(Hash) && poll_result[:tab_action]
|
|
313
|
+
(@pending_inputs ||= []).unshift(poll_result)
|
|
314
|
+
cancellation.cancel!
|
|
315
|
+
end
|
|
316
|
+
sleep 0.01
|
|
317
|
+
end
|
|
318
|
+
worker.join
|
|
319
|
+
drain_ekwsh_chunks(chunks)
|
|
320
|
+
raise error if error
|
|
321
|
+
|
|
322
|
+
queued_inputs.reverse_each { |pending_input| (@pending_inputs ||= []).unshift(pending_input) }
|
|
323
|
+
result
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def drain_ekwsh_chunks(chunks)
|
|
327
|
+
loop do
|
|
328
|
+
@prompt.write_transcript_delta(chunks.pop(true))
|
|
329
|
+
rescue ThreadError
|
|
330
|
+
break
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def pending_tab_action?
|
|
335
|
+
@pending_inputs&.first.is_a?(Hash) && @pending_inputs.first[:tab_action]
|
|
336
|
+
end
|
|
337
|
+
|
|
206
338
|
def configured_workspace(root: current_workspace_root)
|
|
207
339
|
Workspace.new(root: root, guardrails: workspace_guardrails_enabled?)
|
|
208
340
|
end
|
|
@@ -279,7 +411,9 @@ module Kward
|
|
|
279
411
|
def refresh_conversation_runtime(conversation, reasoning_effort: current_reasoning_effort, refresh_system_message: true)
|
|
280
412
|
return unless conversation&.respond_to?(:update_runtime_context!)
|
|
281
413
|
|
|
414
|
+
runtime_changed = [conversation.provider, conversation.model, conversation.reasoning_effort] != [current_model_provider, current_model_id, reasoning_effort]
|
|
282
415
|
conversation.update_runtime_context!(provider: current_model_provider, model: current_model_id, reasoning_effort: reasoning_effort, refresh: refresh_system_message)
|
|
416
|
+
conversation.persist_runtime_context! if runtime_changed && conversation.respond_to?(:persist_runtime_context!)
|
|
283
417
|
update_assistant_prompt(conversation)
|
|
284
418
|
end
|
|
285
419
|
|
|
@@ -292,7 +426,7 @@ module Kward
|
|
|
292
426
|
end
|
|
293
427
|
|
|
294
428
|
def default_session_name(input)
|
|
295
|
-
|
|
429
|
+
SessionNaming.default_name(input)
|
|
296
430
|
end
|
|
297
431
|
|
|
298
432
|
end
|
data/lib/kward/cli/settings.rb
CHANGED
|
@@ -734,7 +734,6 @@ module Kward
|
|
|
734
734
|
refresh_reasoning_status
|
|
735
735
|
else
|
|
736
736
|
refresh_conversation_runtime(conversation, reasoning_effort: effort)
|
|
737
|
-
conversation.persist_runtime_context! if conversation&.respond_to?(:persist_runtime_context!)
|
|
738
737
|
@prompt.redraw if @prompt.respond_to?(:redraw)
|
|
739
738
|
end
|
|
740
739
|
end
|
|
@@ -25,12 +25,21 @@ module Kward
|
|
|
25
25
|
when "git"
|
|
26
26
|
handle_git_command(agent)
|
|
27
27
|
[true, nil]
|
|
28
|
+
when "diff"
|
|
29
|
+
open_session_diff
|
|
30
|
+
[true, nil]
|
|
28
31
|
when "files"
|
|
29
32
|
open_project_files_browser
|
|
30
33
|
[true, nil]
|
|
31
34
|
when "shell"
|
|
32
35
|
run_ekwsh(agent)
|
|
33
36
|
[true, nil]
|
|
37
|
+
when "scratchpad"
|
|
38
|
+
open_scratchpad_command(argument)
|
|
39
|
+
[true, nil]
|
|
40
|
+
when "pty"
|
|
41
|
+
run_interactive_pty_command(argument, agent)
|
|
42
|
+
[true, nil]
|
|
34
43
|
when "workers"
|
|
35
44
|
unless experimental_workers_enabled?
|
|
36
45
|
runtime_output("Workers are experimental. Start Kward with --experimental-workers to enable /workers.")
|
|
@@ -38,6 +47,13 @@ module Kward
|
|
|
38
47
|
end
|
|
39
48
|
|
|
40
49
|
[true, handle_workers_command(argument, agent, session_store)]
|
|
50
|
+
when "queue"
|
|
51
|
+
unless experimental_workers_enabled?
|
|
52
|
+
runtime_output("Worker queues are experimental. Start Kward with --experimental-workers to enable /queue.")
|
|
53
|
+
return [true, nil]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
[true, handle_worker_queue_command(argument, agent, session_store)]
|
|
41
57
|
when "tab"
|
|
42
58
|
[true, handle_tab_command(argument, session_store)]
|
|
43
59
|
when "settings"
|
|
@@ -125,6 +141,44 @@ module Kward
|
|
|
125
141
|
PromptCommands.parse(command) || [nil, ""]
|
|
126
142
|
end
|
|
127
143
|
|
|
144
|
+
def open_session_diff
|
|
145
|
+
unless @active_session&.path
|
|
146
|
+
runtime_output("No active persisted session.")
|
|
147
|
+
return
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
content = SessionDiff.content_from_session_file(@active_session.path)
|
|
151
|
+
if content.empty?
|
|
152
|
+
runtime_output("No file changes recorded in this session.")
|
|
153
|
+
return
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
if @prompt.respond_to?(:open_modal_diff_viewer)
|
|
157
|
+
@prompt.open_modal_diff_viewer("Session diff", content)
|
|
158
|
+
elsif @prompt.respond_to?(:open_diff_viewer)
|
|
159
|
+
@prompt.open_diff_viewer("Session diff", content)
|
|
160
|
+
else
|
|
161
|
+
runtime_output(content)
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def open_scratchpad_command(argument)
|
|
166
|
+
if @prompt.respond_to?(:scratchpad)
|
|
167
|
+
@prompt.scratchpad(scratchpad_language_argument(argument))
|
|
168
|
+
else
|
|
169
|
+
runtime_output("The scratchpad is only available in the interactive prompt.")
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def scratchpad_language_argument(argument)
|
|
174
|
+
value = argument.to_s.strip.downcase
|
|
175
|
+
return :text if value.empty? || value == "text"
|
|
176
|
+
return :markdown if ["markdown", "md"].include?(value)
|
|
177
|
+
return :ruby if ["ruby", "rb"].include?(value)
|
|
178
|
+
|
|
179
|
+
:text
|
|
180
|
+
end
|
|
181
|
+
|
|
128
182
|
def open_project_files_browser
|
|
129
183
|
if @prompt.respond_to?(:open_project_browser)
|
|
130
184
|
@prompt.open_project_browser
|
|
@@ -164,6 +218,165 @@ module Kward
|
|
|
164
218
|
nil
|
|
165
219
|
end
|
|
166
220
|
|
|
221
|
+
def handle_worker_queue_command(argument, agent, session_store)
|
|
222
|
+
action, value = argument.to_s.strip.split(/\s+/, 2)
|
|
223
|
+
case action
|
|
224
|
+
when nil, "", "status", "list"
|
|
225
|
+
show_worker_queue
|
|
226
|
+
when "add", "enqueue"
|
|
227
|
+
enqueue_active_tab(value, agent)
|
|
228
|
+
when "open", "show"
|
|
229
|
+
open_worker_queue_job(session_store, value)
|
|
230
|
+
when "run", "start"
|
|
231
|
+
run_worker_queue(session_store)
|
|
232
|
+
when "resume"
|
|
233
|
+
value.to_s.strip.empty? ? run_worker_queue(session_store) : resume_worker_queue_job(session_store, value)
|
|
234
|
+
when "suspend", "pause"
|
|
235
|
+
suspend_worker_queue_job(session_store, value)
|
|
236
|
+
when "next"
|
|
237
|
+
run_next_worker_queue_job(session_store)
|
|
238
|
+
else
|
|
239
|
+
runtime_output("Usage: /queue [add|list|status|run|suspend <id>|resume <id>]")
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def worker_queue_store
|
|
244
|
+
@worker_queue_store ||= Workers::QueueStore.new
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def enqueue_active_tab(title, agent)
|
|
248
|
+
session = @active_session
|
|
249
|
+
unless session&.path
|
|
250
|
+
runtime_output("No active persisted session to queue.")
|
|
251
|
+
return
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
job = worker_queue_store.enqueue(
|
|
255
|
+
title: worker_queue_title(title, session, agent),
|
|
256
|
+
session_path: session.path,
|
|
257
|
+
workspace_root: session.cwd || current_workspace_root
|
|
258
|
+
)
|
|
259
|
+
runtime_output("Queued worker #{job.id}: #{job.title}")
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def worker_queue_title(title, session, agent)
|
|
263
|
+
explicit = title.to_s.strip
|
|
264
|
+
return explicit unless explicit.empty?
|
|
265
|
+
|
|
266
|
+
session_name = session&.name.to_s.strip
|
|
267
|
+
return session_name unless session_name.empty?
|
|
268
|
+
|
|
269
|
+
last_user = if agent&.respond_to?(:conversation)
|
|
270
|
+
agent.conversation.messages.reverse.find { |message| MessageAccess.role(message) == "user" }
|
|
271
|
+
end
|
|
272
|
+
content = MessageAccess.content(last_user).to_s.strip.gsub(/\s+/, " ")
|
|
273
|
+
content.empty? ? "Queued worker" : content[0, 80]
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def open_worker_queue_job(session_store, id)
|
|
277
|
+
unless session_store
|
|
278
|
+
runtime_output("Worker queue requires persisted sessions.")
|
|
279
|
+
return nil
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
id = id.to_s.strip
|
|
283
|
+
return runtime_output("Usage: /queue open <id>") if id.empty?
|
|
284
|
+
|
|
285
|
+
record = worker_queue_store.find(id)
|
|
286
|
+
return runtime_output("Unknown worker job: #{id}") unless record
|
|
287
|
+
|
|
288
|
+
load_session(session_store, record.fetch("session_path"), message: "Showing queued worker #{record.fetch('id')}")
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def run_worker_queue(session_store)
|
|
292
|
+
unless session_store
|
|
293
|
+
runtime_output("Worker queue requires persisted sessions.")
|
|
294
|
+
return
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
results = worker_queue_runner(session_store).run_all
|
|
298
|
+
if results.empty?
|
|
299
|
+
runtime_output("Worker queue has no queued jobs.")
|
|
300
|
+
return
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
summary = results.map { |record| "#{record.fetch('id')} #{record.fetch('status')}" }.join(", ")
|
|
304
|
+
runtime_output("Worker queue run finished: #{summary}")
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def run_next_worker_queue_job(session_store)
|
|
308
|
+
unless session_store
|
|
309
|
+
runtime_output("Worker queue requires persisted sessions.")
|
|
310
|
+
return
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
record = worker_queue_runner(session_store).run_next
|
|
314
|
+
if record
|
|
315
|
+
runtime_output("Worker #{record.fetch('id')} finished with status #{record.fetch('status')}.")
|
|
316
|
+
else
|
|
317
|
+
runtime_output("Worker queue has no queued jobs.")
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def suspend_worker_queue_job(session_store, id)
|
|
322
|
+
unless session_store
|
|
323
|
+
runtime_output("Worker queue requires persisted sessions.")
|
|
324
|
+
return
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
id = id.to_s.strip
|
|
328
|
+
return runtime_output("Usage: /queue suspend <id>") if id.empty?
|
|
329
|
+
|
|
330
|
+
record = worker_queue_runner(session_store).suspend(id)
|
|
331
|
+
runtime_output("Worker #{record.fetch('id')} suspended.")
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def resume_worker_queue_job(session_store, id)
|
|
335
|
+
unless session_store
|
|
336
|
+
runtime_output("Worker queue requires persisted sessions.")
|
|
337
|
+
return
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
id = id.to_s.strip
|
|
341
|
+
return runtime_output("Usage: /queue resume <id>") if id.empty?
|
|
342
|
+
|
|
343
|
+
record = worker_queue_runner(session_store).resume(id)
|
|
344
|
+
runtime_output("Worker #{record.fetch('id')} finished with status #{record.fetch('status')}.")
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def worker_queue_runner(session_store)
|
|
348
|
+
Workers::QueueRunner.new(
|
|
349
|
+
queue_store: worker_queue_store,
|
|
350
|
+
session_store: session_store,
|
|
351
|
+
client_factory: -> { Client.new },
|
|
352
|
+
prompt: @prompt,
|
|
353
|
+
workspace_root: current_workspace_root,
|
|
354
|
+
provider: current_model_provider,
|
|
355
|
+
model: current_model_id,
|
|
356
|
+
reasoning_effort: current_reasoning_effort,
|
|
357
|
+
write_lock: (@worker_write_lock ||= Workers::WriteLock.new)
|
|
358
|
+
)
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
def show_worker_queue
|
|
362
|
+
jobs = worker_queue_store.list
|
|
363
|
+
if jobs.empty?
|
|
364
|
+
runtime_output("Worker queue is empty.")
|
|
365
|
+
return
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
lines = ["Worker queue:"]
|
|
369
|
+
jobs.each do |job|
|
|
370
|
+
details = [job.fetch("id"), "[#{job.fetch('status')}]"]
|
|
371
|
+
details << "##{job['position']}" if job["position"]
|
|
372
|
+
details << job.fetch("title")
|
|
373
|
+
details << "commit #{job['commit_sha']}" unless job["commit_sha"].to_s.empty?
|
|
374
|
+
details << "error: #{job['error']}" unless job["error"].to_s.empty?
|
|
375
|
+
lines << "- #{details.join(' ')}"
|
|
376
|
+
end
|
|
377
|
+
runtime_output(lines.join("\n"))
|
|
378
|
+
end
|
|
379
|
+
|
|
167
380
|
def handle_workers_command(argument, agent, session_store)
|
|
168
381
|
action, value = argument.to_s.strip.split(/\s+/, 2)
|
|
169
382
|
replacement_agent = case action
|
data/lib/kward/cli/tabs.rb
CHANGED
|
@@ -27,6 +27,7 @@ module Kward
|
|
|
27
27
|
:unread,
|
|
28
28
|
:pending_question,
|
|
29
29
|
:shell,
|
|
30
|
+
:error_reported,
|
|
30
31
|
keyword_init: true
|
|
31
32
|
) do
|
|
32
33
|
def running?
|
|
@@ -148,7 +149,8 @@ module Kward
|
|
|
148
149
|
label: label,
|
|
149
150
|
unread: false,
|
|
150
151
|
pending_question: nil,
|
|
151
|
-
shell: nil
|
|
152
|
+
shell: nil,
|
|
153
|
+
error_reported: false
|
|
152
154
|
).tap { |tab| assign_tab_question_prompt(agent, tab) }
|
|
153
155
|
end
|
|
154
156
|
|
|
@@ -271,6 +273,7 @@ module Kward
|
|
|
271
273
|
tab.error = nil
|
|
272
274
|
tab.answer = nil
|
|
273
275
|
tab.unread = false
|
|
276
|
+
tab.error_reported = false
|
|
274
277
|
tab.event_history.clear
|
|
275
278
|
tab.seen_events = 0
|
|
276
279
|
tab.queued_inputs.clear
|
|
@@ -326,7 +329,7 @@ module Kward
|
|
|
326
329
|
else
|
|
327
330
|
render_conversation_transcript(tab.agent.conversation)
|
|
328
331
|
end
|
|
329
|
-
|
|
332
|
+
report_tab_runtime_error(tab) if %w[failed cancelled].include?(tab.status.to_s)
|
|
330
333
|
end
|
|
331
334
|
restore_tab_composer_snapshot(tab.snapshot)
|
|
332
335
|
end
|
|
@@ -344,8 +347,32 @@ module Kward
|
|
|
344
347
|
end
|
|
345
348
|
end
|
|
346
349
|
|
|
347
|
-
def
|
|
348
|
-
|
|
350
|
+
def report_tab_runtime_error(tab)
|
|
351
|
+
return if tab.error_reported
|
|
352
|
+
|
|
353
|
+
message = tab_runtime_error_message(tab)
|
|
354
|
+
return if message.empty?
|
|
355
|
+
|
|
356
|
+
tab.error_reported = true
|
|
357
|
+
runtime_output(message)
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
def tab_runtime_error_message(tab)
|
|
361
|
+
number = tab_number(tab)
|
|
362
|
+
case tab.status.to_s
|
|
363
|
+
when "failed"
|
|
364
|
+
error = tab.error.to_s.strip
|
|
365
|
+
error.empty? ? "Tab #{number} error." : "Tab #{number} error: #{error}"
|
|
366
|
+
when "cancelled"
|
|
367
|
+
"Tab #{number} cancelled."
|
|
368
|
+
else
|
|
369
|
+
""
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
def tab_number(tab)
|
|
374
|
+
index = @tabs&.index(tab)
|
|
375
|
+
index ? index + 1 : active_tab_number
|
|
349
376
|
end
|
|
350
377
|
|
|
351
378
|
def submit_tab_input(tab, input, display_input: nil)
|
|
@@ -366,6 +393,7 @@ module Kward
|
|
|
366
393
|
tab.steering = steering_supported? ? Steering.new : nil
|
|
367
394
|
tab.error = nil
|
|
368
395
|
tab.answer = nil
|
|
396
|
+
tab.error_reported = false
|
|
369
397
|
tab.event_history.clear
|
|
370
398
|
tab.seen_events = 0
|
|
371
399
|
tab.markdown_chunks.clear
|
|
@@ -390,10 +418,12 @@ module Kward
|
|
|
390
418
|
rescue Cancellation::CancelledError
|
|
391
419
|
tab.status = "cancelled"
|
|
392
420
|
tab.unread = false
|
|
421
|
+
report_tab_runtime_error(tab)
|
|
393
422
|
rescue StandardError => e
|
|
394
423
|
tab.error = e.message
|
|
395
424
|
tab.status = "failed"
|
|
396
425
|
tab.unread = false
|
|
426
|
+
report_tab_runtime_error(tab)
|
|
397
427
|
ensure
|
|
398
428
|
finish_tab_turn(tab)
|
|
399
429
|
end
|
|
@@ -21,6 +21,8 @@ module Kward
|
|
|
21
21
|
shell_command_summary(args, text)
|
|
22
22
|
when "web_search"
|
|
23
23
|
web_search_summary(args, text)
|
|
24
|
+
when "read_skill"
|
|
25
|
+
read_skill_summary(text)
|
|
24
26
|
else
|
|
25
27
|
generic_tool_summary(name, text)
|
|
26
28
|
end
|
|
@@ -76,6 +78,10 @@ module Kward
|
|
|
76
78
|
lines.join("\n")
|
|
77
79
|
end
|
|
78
80
|
|
|
81
|
+
def read_skill_summary(content)
|
|
82
|
+
"read_skill:\n#{content}"
|
|
83
|
+
end
|
|
84
|
+
|
|
79
85
|
def error_tool_summary(name, args, content)
|
|
80
86
|
path = args["path"] || args[:path]
|
|
81
87
|
command = args["command"] || args[:command]
|
data/lib/kward/cli.rb
CHANGED
|
@@ -9,11 +9,13 @@ require_relative "model/client"
|
|
|
9
9
|
require_relative "compactor"
|
|
10
10
|
require_relative "config_files"
|
|
11
11
|
require_relative "clipboard"
|
|
12
|
+
require_relative "cancellation"
|
|
12
13
|
require_relative "cli_transcript_formatter"
|
|
13
14
|
require_relative "model/context_usage"
|
|
14
15
|
require_relative "events"
|
|
15
16
|
require_relative "export_path"
|
|
16
17
|
require_relative "ekwsh"
|
|
18
|
+
require_relative "interactive_pty_runner"
|
|
17
19
|
require_relative "auth/anthropic_oauth"
|
|
18
20
|
require_relative "auth/github_oauth"
|
|
19
21
|
require_relative "auth/openrouter_api_key"
|
|
@@ -30,6 +32,7 @@ require_relative "model/retry_message"
|
|
|
30
32
|
require_relative "rpc/server"
|
|
31
33
|
require_relative "session_diff"
|
|
32
34
|
require_relative "session_store"
|
|
35
|
+
require_relative "session_naming"
|
|
33
36
|
require_relative "tab_store"
|
|
34
37
|
require_relative "session_trash"
|
|
35
38
|
require_relative "session_tree_renderer"
|
|
@@ -185,17 +188,6 @@ module Kward
|
|
|
185
188
|
return
|
|
186
189
|
end
|
|
187
190
|
|
|
188
|
-
if @argv.first == "count-tests"
|
|
189
|
-
if help_option_arguments?(@argv[1..] || [])
|
|
190
|
-
print_command_help("count-tests")
|
|
191
|
-
return
|
|
192
|
-
end
|
|
193
|
-
raise ArgumentError, command_usage("count-tests") unless @argv.length == 1
|
|
194
|
-
|
|
195
|
-
print_test_count
|
|
196
|
-
return
|
|
197
|
-
end
|
|
198
|
-
|
|
199
191
|
if @argv.first == "sysprompt"
|
|
200
192
|
if help_option_arguments?(@argv[1..] || [])
|
|
201
193
|
print_command_help("sysprompt")
|
|
@@ -383,7 +375,7 @@ module Kward
|
|
|
383
375
|
|
|
384
376
|
loop do
|
|
385
377
|
if @pending_inputs.empty? && active_tab&.shell
|
|
386
|
-
run_ekwsh_loop(active_tab.shell, tab: active_tab)
|
|
378
|
+
run_ekwsh_loop(active_tab.shell, tab: active_tab, history: build_ekwsh_history(active_tab.agent))
|
|
387
379
|
end
|
|
388
380
|
input = @pending_inputs.shift || (active_tab ? poll_active_tab_input : @prompt.ask("You>"))
|
|
389
381
|
if input.is_a?(Hash) && input[:tab_action]
|
data/lib/kward/clipboard.rb
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
require "base64"
|
|
2
1
|
require "open3"
|
|
2
|
+
require_relative "terminal_sequences"
|
|
3
3
|
require "rbconfig"
|
|
4
4
|
|
|
5
5
|
# Namespace for the Kward CLI agent runtime.
|
|
@@ -39,8 +39,7 @@ module Kward
|
|
|
39
39
|
end
|
|
40
40
|
|
|
41
41
|
def write_osc52(content)
|
|
42
|
-
|
|
43
|
-
@output.print("\e]52;c;#{encoded}\a")
|
|
42
|
+
@output.print(TerminalSequences.osc52(content))
|
|
44
43
|
@output.flush if @output.respond_to?(:flush)
|
|
45
44
|
end
|
|
46
45
|
|