kward 0.71.0 → 0.72.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 +41 -1
- data/Gemfile.lock +2 -2
- data/README.md +4 -0
- data/doc/agent-tools.md +15 -6
- data/doc/authentication.md +22 -1
- data/doc/code-search.md +42 -2
- data/doc/configuration.md +106 -3
- data/doc/context-budgeting.md +136 -0
- data/doc/context-tools.md +16 -3
- data/doc/editor.md +394 -0
- data/doc/extensibility.md +16 -7
- data/doc/files.md +100 -0
- data/doc/getting-started.md +25 -18
- data/doc/git.md +122 -0
- data/doc/memory.md +24 -4
- data/doc/personas.md +34 -5
- data/doc/plugins.md +72 -1
- data/doc/releasing.md +37 -9
- data/doc/rpc.md +74 -4
- data/doc/session-management.md +35 -1
- data/doc/shell.md +286 -0
- data/doc/tabs.md +122 -0
- data/doc/troubleshooting.md +77 -1
- data/doc/usage.md +53 -7
- data/doc/web-search.md +12 -4
- data/doc/workspace-tools.md +51 -12
- data/examples/plugins/space_invaders.rb +377 -0
- data/lib/kward/agent.rb +1 -1
- data/lib/kward/cli/commands.rb +33 -2
- data/lib/kward/cli/git.rb +150 -0
- data/lib/kward/cli/interactive_turn.rb +73 -9
- data/lib/kward/cli/plugins.rb +54 -4
- data/lib/kward/cli/prompt_interface.rb +32 -1
- data/lib/kward/cli/runtime_helpers.rb +133 -3
- data/lib/kward/cli/sessions.rb +2 -2
- data/lib/kward/cli/settings.rb +218 -9
- data/lib/kward/cli/slash_commands.rb +415 -2
- data/lib/kward/cli/tabs.rb +695 -0
- data/lib/kward/cli.rb +158 -26
- data/lib/kward/config_files.rb +123 -1
- data/lib/kward/context_budget_meter.rb +44 -0
- data/lib/kward/conversation.rb +12 -4
- data/lib/kward/editor_mode.rb +25 -0
- data/lib/kward/ekwsh.rb +362 -0
- data/lib/kward/plugin_registry.rb +61 -0
- data/lib/kward/project_files.rb +52 -0
- data/lib/kward/prompt_history.rb +82 -0
- data/lib/kward/prompt_interface/composer_controller.rb +69 -1
- data/lib/kward/prompt_interface/composer_renderer.rb +109 -13
- data/lib/kward/prompt_interface/composer_state.rb +96 -27
- data/lib/kward/prompt_interface/editor/auto_close_pairs.rb +123 -0
- data/lib/kward/prompt_interface/editor/auto_indent.rb +509 -0
- data/lib/kward/prompt_interface/editor/buffer.rb +109 -0
- data/lib/kward/prompt_interface/editor/controller.rb +1018 -0
- data/lib/kward/prompt_interface/editor/endwise.rb +321 -0
- data/lib/kward/prompt_interface/editor/file_marker.rb +40 -0
- data/lib/kward/prompt_interface/editor/indent_navigation.rb +61 -0
- data/lib/kward/prompt_interface/editor/kill_ring.rb +78 -0
- data/lib/kward/prompt_interface/editor/modes/emacs.rb +259 -0
- data/lib/kward/prompt_interface/editor/modes/modern.rb +353 -0
- data/lib/kward/prompt_interface/editor/modes/vibe.rb +1962 -0
- data/lib/kward/prompt_interface/editor/renderer.rb +243 -0
- data/lib/kward/prompt_interface/editor/search.rb +76 -0
- data/lib/kward/prompt_interface/editor/selections.rb +120 -0
- data/lib/kward/prompt_interface/editor/state.rb +1249 -0
- data/lib/kward/prompt_interface/editor/status_text.rb +23 -0
- data/lib/kward/prompt_interface/editor/syntax_highlighter.rb +420 -0
- data/lib/kward/prompt_interface/editor/undo_history.rb +46 -0
- data/lib/kward/prompt_interface/editor/vibe_state.rb +44 -0
- data/lib/kward/prompt_interface/file_overlay.rb +211 -0
- data/lib/kward/prompt_interface/git_prompt.rb +299 -0
- data/lib/kward/prompt_interface/interactive/controller.rb +186 -0
- data/lib/kward/prompt_interface/interactive/renderer.rb +71 -0
- data/lib/kward/prompt_interface/interactive/state.rb +62 -0
- data/lib/kward/prompt_interface/key_handler.rb +387 -35
- data/lib/kward/prompt_interface/overlay_renderer.rb +21 -2
- data/lib/kward/prompt_interface/project_browser.rb +524 -0
- data/lib/kward/prompt_interface/question_prompt.rb +98 -50
- data/lib/kward/prompt_interface/runtime_state.rb +43 -0
- data/lib/kward/prompt_interface/screen.rb +16 -0
- data/lib/kward/prompt_interface/selection_prompt.rb +7 -13
- data/lib/kward/prompt_interface/stream_state.rb +7 -0
- data/lib/kward/prompt_interface/transcript_buffer.rb +6 -0
- data/lib/kward/prompt_interface.rb +286 -8
- data/lib/kward/prompts/commands.rb +5 -0
- data/lib/kward/prompts.rb +2 -0
- data/lib/kward/rpc/server.rb +42 -3
- data/lib/kward/rpc/session_manager.rb +35 -47
- data/lib/kward/rpc/session_tree_rows.rb +9 -115
- data/lib/kward/rpc/tool_event_normalizer.rb +1 -1
- data/lib/kward/session_store.rb +44 -0
- data/lib/kward/session_tree_nodes.rb +136 -0
- data/lib/kward/session_tree_renderer.rb +9 -131
- data/lib/kward/tab_store.rb +47 -0
- data/lib/kward/text_boundary.rb +25 -0
- data/lib/kward/tools/context_budget_stats.rb +54 -0
- data/lib/kward/tools/context_for_task.rb +202 -0
- data/lib/kward/tools/read_file.rb +8 -4
- data/lib/kward/tools/registry.rb +62 -16
- data/lib/kward/tools/tool_call.rb +10 -0
- data/lib/kward/version.rb +1 -1
- data/lib/kward/workers/git_guard.rb +68 -0
- data/lib/kward/workers/live_view.rb +49 -0
- data/lib/kward/workers/manager.rb +288 -0
- data/lib/kward/workers/store.rb +72 -0
- data/lib/kward/workers/tool_policy.rb +23 -0
- data/lib/kward/workers/worker.rb +82 -0
- data/lib/kward/workers/write_lock.rb +38 -0
- data/lib/kward/workers.rb +7 -0
- data/lib/kward/workspace.rb +110 -24
- data/templates/default/fulldoc/html/css/kward.css +107 -36
- data/templates/default/kward_navigation.rb +12 -1
- data/templates/default/layout/html/layout.erb +4 -2
- data/templates/default/layout/html/setup.rb +6 -0
- metadata +53 -1
|
@@ -22,6 +22,24 @@ module Kward
|
|
|
22
22
|
when "redraw"
|
|
23
23
|
run_busy_local_command_and_requeue { @prompt.redraw if @prompt.respond_to?(:redraw) }
|
|
24
24
|
[true, nil]
|
|
25
|
+
when "git"
|
|
26
|
+
handle_git_command(agent)
|
|
27
|
+
[true, nil]
|
|
28
|
+
when "files"
|
|
29
|
+
open_project_files_browser
|
|
30
|
+
[true, nil]
|
|
31
|
+
when "shell"
|
|
32
|
+
run_ekwsh(agent)
|
|
33
|
+
[true, nil]
|
|
34
|
+
when "workers"
|
|
35
|
+
unless experimental_workers_enabled?
|
|
36
|
+
runtime_output("Workers are experimental. Start Kward with --experimental-workers to enable /workers.")
|
|
37
|
+
return [true, nil]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
[true, handle_workers_command(argument, agent, session_store)]
|
|
41
|
+
when "tab"
|
|
42
|
+
[true, handle_tab_command(argument, session_store)]
|
|
25
43
|
when "settings"
|
|
26
44
|
configure_settings(agent.conversation)
|
|
27
45
|
[true, nil]
|
|
@@ -93,7 +111,9 @@ module Kward
|
|
|
93
111
|
run_busy_local_command_and_requeue(activity: "compacting") { compact_context(agent, argument) }
|
|
94
112
|
[true, nil]
|
|
95
113
|
else
|
|
96
|
-
if
|
|
114
|
+
if interactive_command_for(name) && prompt_interface? && @prompt.respond_to?(:start_interactive)
|
|
115
|
+
run_interactive_command(name, argument, agent)
|
|
116
|
+
elsif plugin_command_for(name)
|
|
97
117
|
run_busy_local_command_and_requeue(activity: "running") { run_plugin_command(name, argument, agent) }
|
|
98
118
|
else
|
|
99
119
|
[false, nil]
|
|
@@ -105,9 +125,17 @@ module Kward
|
|
|
105
125
|
PromptCommands.parse(command) || [nil, ""]
|
|
106
126
|
end
|
|
107
127
|
|
|
128
|
+
def open_project_files_browser
|
|
129
|
+
if @prompt.respond_to?(:open_project_browser)
|
|
130
|
+
@prompt.open_project_browser
|
|
131
|
+
else
|
|
132
|
+
runtime_output("The project file browser is only available in the interactive prompt.")
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
108
136
|
# Writes the status output for the terminal CLI flow.
|
|
109
137
|
def print_status
|
|
110
|
-
lines = [
|
|
138
|
+
lines = ["Kward status"]
|
|
111
139
|
lines << ""
|
|
112
140
|
lines << auto_compaction_status_line
|
|
113
141
|
if @active_session
|
|
@@ -136,6 +164,391 @@ module Kward
|
|
|
136
164
|
nil
|
|
137
165
|
end
|
|
138
166
|
|
|
167
|
+
def handle_workers_command(argument, agent, session_store)
|
|
168
|
+
action, value = argument.to_s.strip.split(/\s+/, 2)
|
|
169
|
+
replacement_agent = case action
|
|
170
|
+
when nil, ""
|
|
171
|
+
open_worker_menu(agent, session_store)
|
|
172
|
+
when "list"
|
|
173
|
+
open_worker_list(agent, session_store)
|
|
174
|
+
when "new", "do"
|
|
175
|
+
prompt_for_worker_request(agent, value)
|
|
176
|
+
nil
|
|
177
|
+
else
|
|
178
|
+
runtime_output("Usage: /workers | /workers new | /workers list")
|
|
179
|
+
nil
|
|
180
|
+
end
|
|
181
|
+
replacement_agent?(replacement_agent) ? replacement_agent : nil
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def worker_store
|
|
185
|
+
@worker_store ||= Workers::Store.new
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def worker_manager(agent)
|
|
189
|
+
workspace_root = interactive_workspace_root(agent)
|
|
190
|
+
return @worker_manager if @worker_manager && @worker_manager_workspace_root == workspace_root
|
|
191
|
+
|
|
192
|
+
@worker_manager_workspace_root = workspace_root
|
|
193
|
+
@worker_manager = Workers::Manager.new(
|
|
194
|
+
client_factory: -> { Client.new },
|
|
195
|
+
prompt: @prompt,
|
|
196
|
+
workspace_root: workspace_root,
|
|
197
|
+
session_store: interactive_session_store(agent),
|
|
198
|
+
provider: current_model_provider,
|
|
199
|
+
model: current_model_id,
|
|
200
|
+
reasoning_effort: current_reasoning_effort,
|
|
201
|
+
write_lock: (@worker_write_lock ||= Workers::WriteLock.new),
|
|
202
|
+
worker_store: worker_store,
|
|
203
|
+
write_lane_available: -> { !@foreground_turn_active }
|
|
204
|
+
)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def open_worker_menu(agent, session_store)
|
|
208
|
+
return runtime_output("Usage: /workers | /workers new | /workers list") unless @prompt.respond_to?(:select)
|
|
209
|
+
|
|
210
|
+
choice = @prompt.select(
|
|
211
|
+
"Workers",
|
|
212
|
+
["New worker", "List workers"],
|
|
213
|
+
title: "Workers",
|
|
214
|
+
custom: false
|
|
215
|
+
)
|
|
216
|
+
case choice
|
|
217
|
+
when "New worker"
|
|
218
|
+
prompt_for_worker_request(agent)
|
|
219
|
+
when "List workers"
|
|
220
|
+
open_worker_list(agent, session_store)
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def prompt_for_worker_request(agent, topic = nil)
|
|
225
|
+
topic = @prompt.ask("Worker task>") if topic.to_s.strip.empty? && @prompt.respond_to?(:ask)
|
|
226
|
+
send_worker_request(topic, agent) unless topic.to_s.strip.empty?
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def send_worker_request(topic, agent)
|
|
230
|
+
if topic.to_s.strip.empty?
|
|
231
|
+
runtime_output("Usage: /workers new <task>")
|
|
232
|
+
return
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
worker = worker_manager(agent).start(role: "request", prompt: topic)
|
|
236
|
+
runtime_output("Worker #{worker.id} started: #{worker.title}")
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def open_worker_list(agent, session_store, title: "Workers", empty_message: "No workers in the pipeline.")
|
|
240
|
+
return runtime_output(empty_message) unless @prompt.respond_to?(:select)
|
|
241
|
+
|
|
242
|
+
jobs = worker_jobs(agent)
|
|
243
|
+
if jobs.empty?
|
|
244
|
+
runtime_output(empty_message)
|
|
245
|
+
return
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
labels = jobs.map { |job| worker_choice_label(job) }
|
|
249
|
+
choice = @prompt.select("Select worker", labels, title: title, custom: false)
|
|
250
|
+
return unless choice
|
|
251
|
+
|
|
252
|
+
selected = jobs[labels.index(choice)]
|
|
253
|
+
open_worker_actions(selected, agent, session_store) if selected
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def worker_jobs(agent)
|
|
257
|
+
runtime_worker_ids = @worker_manager ? @worker_manager.list.map(&:id) : []
|
|
258
|
+
persisted_workers = worker_store.list.reject { |job| runtime_worker_ids.include?(job["id"]) }
|
|
259
|
+
live_workers = @worker_manager ? @worker_manager.list.map(&:to_h) : []
|
|
260
|
+
[implementation_worker_job(agent)].compact + persisted_workers + live_workers
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def implementation_worker_job(agent)
|
|
264
|
+
remember_implementation_worker(agent) if implementation_agent?(agent)
|
|
265
|
+
path = @implementation_worker_session_path || @active_session&.path
|
|
266
|
+
return nil if path.to_s.empty?
|
|
267
|
+
|
|
268
|
+
{
|
|
269
|
+
"id" => "implementation",
|
|
270
|
+
"title" => @implementation_worker_title || @active_session&.name || "Implementation",
|
|
271
|
+
"role" => "implementation",
|
|
272
|
+
"status" => implementation_agent?(agent) ? "active" : "idle",
|
|
273
|
+
"session_path" => path
|
|
274
|
+
}
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def implementation_agent?(agent)
|
|
278
|
+
@active_worker_role.to_s.empty? || @active_worker_role == "implementation"
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def remember_implementation_worker(agent)
|
|
282
|
+
return unless @active_session&.path
|
|
283
|
+
return unless implementation_agent?(agent)
|
|
284
|
+
|
|
285
|
+
@implementation_worker_session_path = @active_session.path
|
|
286
|
+
@implementation_worker_title = @active_session.name || "Implementation"
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def open_worker_actions(job, _agent, session_store)
|
|
290
|
+
return open_implementation_actions(job, session_store) if job["id"] == "implementation"
|
|
291
|
+
|
|
292
|
+
open_background_worker_actions(job, session_store)
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def open_implementation_actions(job, session_store)
|
|
296
|
+
actions = ["Show", "Back to list"]
|
|
297
|
+
choice = @prompt.select("#{job.fetch('id')} — #{job.fetch('title')}", actions, title: "Worker", custom: false)
|
|
298
|
+
case choice
|
|
299
|
+
when "Show"
|
|
300
|
+
load_implementation_session(session_store, job)
|
|
301
|
+
when "Back to list"
|
|
302
|
+
open_worker_list(nil, session_store)
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def handle_request_worker_input(input, agent, session_store)
|
|
307
|
+
return [false, nil] unless @active_worker_role == "request"
|
|
308
|
+
|
|
309
|
+
worker = visible_request_worker(agent)
|
|
310
|
+
return [false, nil] unless worker
|
|
311
|
+
|
|
312
|
+
text = input.to_s.strip
|
|
313
|
+
return [true, nil] if text.empty?
|
|
314
|
+
return [true, proceed_request_worker(worker.to_h, agent, session_store)] if proceed_request_input?(text)
|
|
315
|
+
|
|
316
|
+
runtime_output("Worker #{worker.id} is a read-only request review. Reply yes/proceed to queue implementation, or use /workers to switch workers.")
|
|
317
|
+
[true, nil]
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def visible_request_worker(agent)
|
|
321
|
+
worker = @visible_worker
|
|
322
|
+
return worker if worker&.role == "request" && worker.status == "ready"
|
|
323
|
+
|
|
324
|
+
id = @visible_worker_id.to_s
|
|
325
|
+
return nil if id.empty? || id == "implementation"
|
|
326
|
+
|
|
327
|
+
worker = @worker_manager&.find(id)
|
|
328
|
+
return worker if worker&.role == "request" && worker.status == "ready"
|
|
329
|
+
|
|
330
|
+
job = worker_store.find(id)
|
|
331
|
+
return nil unless job && job["role"] == "request" && job["status"] == "ready"
|
|
332
|
+
return nil if job["session_path"].to_s.empty? || !session_matches_agent?(job["session_path"], agent)
|
|
333
|
+
|
|
334
|
+
Workers::Worker.new(
|
|
335
|
+
id: job.fetch("id"),
|
|
336
|
+
title: job.fetch("title"),
|
|
337
|
+
role: job.fetch("role"),
|
|
338
|
+
workspace_root: job["workspace_root"] || current_workspace_root,
|
|
339
|
+
status: job.fetch("status"),
|
|
340
|
+
prompt: job["prompt"]
|
|
341
|
+
).tap { |restored| restored.update_status("ready", report: job["report"], error: job["error"]) }
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def session_matches_agent?(path, agent)
|
|
345
|
+
return true unless agent.respond_to?(:conversation)
|
|
346
|
+
return false unless @active_session&.path
|
|
347
|
+
|
|
348
|
+
File.expand_path(path) == File.expand_path(@active_session.path)
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def proceed_request_input?(input)
|
|
352
|
+
input.downcase.strip.match?(/\A(?:y|yes|yeah|yep|sure|ok|okay|go ahead|proceed|continue|implement|do it|please do|make it so)\b/)
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
def proceed_request_worker(job, agent, session_store)
|
|
356
|
+
return runtime_output("Worker #{job.fetch('id')} is not ready to proceed.") unless request_ready?(job)
|
|
357
|
+
|
|
358
|
+
release_implementation_writer
|
|
359
|
+
manager = worker_manager(agent || build_session_agent_for_worker(job, session_store))
|
|
360
|
+
worker = manager.continue(
|
|
361
|
+
job.fetch("id"),
|
|
362
|
+
role: "implementation",
|
|
363
|
+
prompt: implementation_prompt_for_request(job),
|
|
364
|
+
title: "Implement #{job.fetch('title')}"
|
|
365
|
+
)
|
|
366
|
+
runtime_output("Worker #{worker.id} queued from request #{job.fetch('id')}: #{worker.title}")
|
|
367
|
+
wait_for_worker_session(worker)
|
|
368
|
+
load_worker_session(session_store, worker.session.path, worker.to_h, worker: worker) if worker.session&.path
|
|
369
|
+
rescue StandardError => e
|
|
370
|
+
runtime_output("Error: #{e.message}")
|
|
371
|
+
nil
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def wait_for_worker_session(worker, timeout: 1.0)
|
|
375
|
+
deadline = Time.now + timeout
|
|
376
|
+
until worker.session&.path || Time.now >= deadline
|
|
377
|
+
sleep 0.02
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
def build_session_agent_for_worker(job, session_store)
|
|
382
|
+
conversation = Conversation.new(workspace_root: job["workspace_root"] || session_store&.cwd || current_workspace_root)
|
|
383
|
+
build_worker_agent(conversation, role: "request")
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def request_ready?(job)
|
|
387
|
+
job["role"] == "request" && job["status"] == "ready"
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
def implementation_prompt_for_request(job)
|
|
391
|
+
<<~PROMPT
|
|
392
|
+
The user reviewed and approved this Kward request. Continue in the write-capable implementation lane.
|
|
393
|
+
|
|
394
|
+
Original request:
|
|
395
|
+
#{job["prompt"]}
|
|
396
|
+
|
|
397
|
+
Request review:
|
|
398
|
+
#{job["report"].to_s.empty? ? "No saved review text is available. Use the request session transcript for context if needed." : job["report"]}
|
|
399
|
+
|
|
400
|
+
Implement the approved next step. Make the smallest correct change, preserve existing style, and run focused verification when practical.
|
|
401
|
+
If you change files, commit the changes and report the commit hash. If no file changes are needed, explain why.
|
|
402
|
+
PROMPT
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
def open_background_worker_actions(job, session_store)
|
|
406
|
+
actions = ["Show"]
|
|
407
|
+
actions << "Proceed" if request_ready?(job)
|
|
408
|
+
actions << "Cancel" if %w[queued running].include?(job["status"])
|
|
409
|
+
actions << "Dismiss"
|
|
410
|
+
actions << "Back to list"
|
|
411
|
+
choice = @prompt.select("#{job.fetch('id')} — #{job.fetch('title')}", actions, title: "Worker", custom: false)
|
|
412
|
+
case choice
|
|
413
|
+
when "Show"
|
|
414
|
+
worker = @worker_manager&.find(job.fetch("id"))
|
|
415
|
+
path = job["session_path"] || worker&.session&.path
|
|
416
|
+
return runtime_output("Worker #{job.fetch('id')} session is not ready yet.") unless path
|
|
417
|
+
|
|
418
|
+
load_worker_session(session_store, path, job, worker: worker)
|
|
419
|
+
when "Proceed"
|
|
420
|
+
proceed_request_worker(job, nil, session_store)
|
|
421
|
+
when "Cancel"
|
|
422
|
+
@worker_manager&.cancel(job.fetch("id"))
|
|
423
|
+
runtime_output("Worker #{job.fetch('id')} cancelled.")
|
|
424
|
+
when "Dismiss"
|
|
425
|
+
dismiss_worker(job.fetch("id"))
|
|
426
|
+
runtime_output("Worker #{job.fetch('id')} dismissed.")
|
|
427
|
+
when "Back to list"
|
|
428
|
+
open_worker_list(nil, session_store)
|
|
429
|
+
end
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
def dismiss_worker(id)
|
|
433
|
+
@worker_manager&.archive(id)
|
|
434
|
+
rescue ArgumentError
|
|
435
|
+
nil
|
|
436
|
+
ensure
|
|
437
|
+
worker_store.archive(id)
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
def load_implementation_session(session_store, job)
|
|
441
|
+
return runtime_output("Implementation session unavailable.") unless session_store
|
|
442
|
+
|
|
443
|
+
stop_live_worker_view
|
|
444
|
+
@active_worker_role = "implementation"
|
|
445
|
+
set_visible_worker("implementation", status: "active")
|
|
446
|
+
load_session(session_store, job.fetch("session_path"), message: "Showing implementation worker")
|
|
447
|
+
rescue StandardError => e
|
|
448
|
+
runtime_output("Error: #{e.message}")
|
|
449
|
+
nil
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
def worker_choice_label(job)
|
|
453
|
+
error = job["status"] == "failed" && !job["error"].to_s.empty? ? " — #{job['error']}" : ""
|
|
454
|
+
"#{job.fetch('id')} [#{job.fetch('role')}/#{job.fetch('status')}] #{job.fetch('title')}#{error}"
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
def load_worker_session(session_store, path, job, worker: nil)
|
|
458
|
+
unless session_store
|
|
459
|
+
runtime_output(worker_report_text(job))
|
|
460
|
+
return nil
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
release_implementation_writer
|
|
464
|
+
agent = load_session(session_store, path, message: "Showing worker #{job.fetch('id')}")
|
|
465
|
+
release_implementation_writer
|
|
466
|
+
role = visible_session_role(job)
|
|
467
|
+
agent = build_worker_agent(agent.conversation, role: role)
|
|
468
|
+
@active_worker_role = role
|
|
469
|
+
set_visible_worker(job.fetch("id"), status: job["status"], worker: worker)
|
|
470
|
+
@prompt.redraw if @prompt.respond_to?(:redraw)
|
|
471
|
+
start_live_worker_view(worker, agent) if live_worker?(worker)
|
|
472
|
+
agent
|
|
473
|
+
rescue StandardError => e
|
|
474
|
+
runtime_output("Error: #{e.message}")
|
|
475
|
+
nil
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
def visible_session_role(job)
|
|
479
|
+
return "read_only" if job["id"] != "implementation" && job["role"] == "implementation"
|
|
480
|
+
|
|
481
|
+
job["role"] || "request"
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
def live_worker?(worker)
|
|
485
|
+
worker && %w[queued running].include?(worker.status)
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
def start_live_worker_view(worker, agent)
|
|
489
|
+
return unless prompt_interface?
|
|
490
|
+
|
|
491
|
+
stop_live_worker_view
|
|
492
|
+
renderer = live_worker_renderer(worker)
|
|
493
|
+
@live_worker_view = Workers::LiveView.new(worker: worker, agent: agent, renderer: renderer).start
|
|
494
|
+
@prompt.redraw if @prompt.respond_to?(:redraw)
|
|
495
|
+
runtime_output("Watching worker #{worker.id}; the view will update until it finishes.")
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
def stop_live_worker_view
|
|
499
|
+
@live_worker_view&.stop
|
|
500
|
+
@live_worker_view = nil
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
def live_worker_renderer(worker)
|
|
504
|
+
markdown_chunks = []
|
|
505
|
+
stream_state = {
|
|
506
|
+
streamed: false,
|
|
507
|
+
last_flush: monotonic_now,
|
|
508
|
+
stream_block_open: false,
|
|
509
|
+
markdown_streams: {},
|
|
510
|
+
defer_assistant_streaming: false
|
|
511
|
+
}
|
|
512
|
+
lambda do |event, agent|
|
|
513
|
+
if event == :flush
|
|
514
|
+
flush_interactive_markdown_deltas(markdown_chunks, stream_state, force: worker_finished?(worker))
|
|
515
|
+
@prompt.redraw if @prompt.respond_to?(:redraw)
|
|
516
|
+
next
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
notify_plugin_transcript_event(event, agent.respond_to?(:conversation) ? agent.conversation : nil)
|
|
520
|
+
handle_live_worker_event(event, markdown_chunks, stream_state)
|
|
521
|
+
flush_interactive_markdown_deltas(markdown_chunks, stream_state, force: worker_finished?(worker))
|
|
522
|
+
rescue StandardError => e
|
|
523
|
+
runtime_output("Worker view error: #{e.message}")
|
|
524
|
+
end
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
def handle_live_worker_event(event, markdown_chunks, stream_state)
|
|
528
|
+
case event
|
|
529
|
+
when Events::AssistantMessage
|
|
530
|
+
return if stream_state[:streamed]
|
|
531
|
+
|
|
532
|
+
render_assistant_message(event.message)
|
|
533
|
+
else
|
|
534
|
+
handle_interactive_event(event, markdown_chunks, stream_state)
|
|
535
|
+
end
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
def worker_finished?(worker)
|
|
539
|
+
%w[ready failed cancelled archived].include?(worker.status)
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
def worker_report_text(job)
|
|
543
|
+
lines = ["Worker #{job.fetch('id')} [#{job.fetch('status')}] #{job.fetch('title')}", ""]
|
|
544
|
+
if job["report"].to_s.empty?
|
|
545
|
+
lines << (job["error"].to_s.empty? ? "No report yet." : "Error: #{job['error']}")
|
|
546
|
+
else
|
|
547
|
+
lines << job["report"]
|
|
548
|
+
end
|
|
549
|
+
lines.join("\n")
|
|
550
|
+
end
|
|
551
|
+
|
|
139
552
|
end
|
|
140
553
|
end
|
|
141
554
|
end
|