kward 0.71.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 +93 -0
- 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 +415 -0
- data/doc/extensibility.md +16 -7
- data/doc/files.md +100 -0
- data/doc/getting-started.md +25 -18
- data/doc/git.md +123 -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 +75 -5
- data/doc/session-management.md +35 -1
- data/doc/shell.md +332 -0
- data/doc/tabs.md +122 -0
- data/doc/troubleshooting.md +77 -1
- data/doc/usage.md +79 -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/ansi.rb +62 -23
- 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/rendering.rb +4 -1
- data/lib/kward/cli/runtime_helpers.rb +268 -4
- data/lib/kward/cli/sessions.rb +2 -2
- data/lib/kward/cli/settings.rb +217 -9
- data/lib/kward/cli/slash_commands.rb +628 -2
- data/lib/kward/cli/tabs.rb +725 -0
- data/lib/kward/cli/tool_summaries.rb +6 -0
- data/lib/kward/cli.rb +150 -26
- data/lib/kward/clipboard.rb +2 -3
- data/lib/kward/compactor.rb +7 -19
- data/lib/kward/config_files.rb +145 -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 +559 -0
- 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/plugin_registry.rb +61 -0
- data/lib/kward/project_files.rb +52 -0
- data/lib/kward/prompt_history.rb +84 -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 +510 -0
- data/lib/kward/prompt_interface/editor/buffer.rb +109 -0
- data/lib/kward/prompt_interface/editor/controller.rb +1218 -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 +354 -0
- data/lib/kward/prompt_interface/editor/modes/vibe.rb +1812 -0
- data/lib/kward/prompt_interface/editor/modes/vibe_insert_readline.rb +166 -0
- data/lib/kward/prompt_interface/editor/renderer.rb +244 -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 +1271 -0
- data/lib/kward/prompt_interface/editor/status_text.rb +23 -0
- data/lib/kward/prompt_interface/editor/syntax_highlighter.rb +422 -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 +288 -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 +451 -57
- 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 +99 -56
- data/lib/kward/prompt_interface/runtime_state.rb +43 -0
- data/lib/kward/prompt_interface/screen.rb +19 -3
- data/lib/kward/prompt_interface/selection_prompt.rb +10 -19
- data/lib/kward/prompt_interface/slash_overlay.rb +2 -0
- 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 +366 -222
- data/lib/kward/prompts/commands.rb +9 -0
- data/lib/kward/prompts.rb +2 -0
- data/lib/kward/rpc/memory_methods.rb +83 -0
- data/lib/kward/rpc/server.rb +169 -83
- data/lib/kward/rpc/session_manager.rb +45 -121
- data/lib/kward/rpc/session_tree_rows.rb +9 -115
- data/lib/kward/rpc/tool_event_normalizer.rb +1 -1
- 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/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/terminal_keys.rb +84 -0
- data/lib/kward/terminal_sequences.rb +42 -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 +204 -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 +93 -0
- data/lib/kward/workers/job.rb +99 -0
- data/lib/kward/workers/live_view.rb +49 -0
- data/lib/kward/workers/manager.rb +288 -0
- data/lib/kward/workers/queue_runner.rb +166 -0
- data/lib/kward/workers/queue_store.rb +112 -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 +10 -0
- data/lib/kward/workspace.rb +125 -87
- data/templates/default/fulldoc/html/css/kward.css +140 -36
- data/templates/default/fulldoc/html/images/kward_screen_1.png +0 -0
- data/templates/default/fulldoc/html/setup.rb +1 -0
- data/templates/default/kward_navigation.rb +12 -1
- data/templates/default/layout/html/layout.erb +23 -34
- data/templates/default/layout/html/setup.rb +6 -0
- metadata +67 -1
|
@@ -43,9 +43,22 @@ module Kward
|
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
def build_interactive_agent(conversation)
|
|
46
|
+
@active_worker_role = "implementation"
|
|
47
|
+
set_visible_worker("implementation", status: "active")
|
|
48
|
+
build_worker_agent(conversation, role: "implementation")
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def build_worker_agent(conversation, role: "implementation")
|
|
46
52
|
conversation.plugin_registry ||= plugin_registry if conversation.respond_to?(:plugin_registry)
|
|
47
53
|
workspace = configured_workspace(root: conversation.workspace_root)
|
|
48
|
-
|
|
54
|
+
writer_id = worker_writer_id(role)
|
|
55
|
+
tool_registry = ToolRegistry.new(
|
|
56
|
+
workspace: workspace,
|
|
57
|
+
prompt: @prompt,
|
|
58
|
+
allowed_tool_names: Workers::ToolPolicy.allowed_tool_names(role),
|
|
59
|
+
write_lock: @worker_write_lock,
|
|
60
|
+
writer_id: writer_id
|
|
61
|
+
)
|
|
49
62
|
@footer_conversation = conversation
|
|
50
63
|
@footer_tool_registry = tool_registry
|
|
51
64
|
Agent.new(
|
|
@@ -55,6 +68,34 @@ module Kward
|
|
|
55
68
|
)
|
|
56
69
|
end
|
|
57
70
|
|
|
71
|
+
def set_visible_worker(id, status: nil, worker: nil)
|
|
72
|
+
@visible_worker_id = id.to_s
|
|
73
|
+
@visible_worker_status = status
|
|
74
|
+
@visible_worker = worker
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def worker_writer_id(role)
|
|
78
|
+
return nil unless Workers::ToolPolicy.write_capable?(role)
|
|
79
|
+
|
|
80
|
+
@worker_write_lock ||= Workers::WriteLock.new
|
|
81
|
+
owner_id = role.to_s.empty? ? "implementation" : role.to_s
|
|
82
|
+
return owner_id if @worker_write_lock.acquire(owner_id)
|
|
83
|
+
|
|
84
|
+
nil
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def refresh_implementation_writer(agent)
|
|
88
|
+
return agent unless @active_worker_role == "implementation"
|
|
89
|
+
return agent unless agent&.respond_to?(:tool_registry)
|
|
90
|
+
return agent if agent.tool_registry.writer_id && @worker_write_lock&.owned_by?(agent.tool_registry.writer_id)
|
|
91
|
+
|
|
92
|
+
build_interactive_agent(agent.conversation)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def release_implementation_writer
|
|
96
|
+
@worker_write_lock&.release("implementation")
|
|
97
|
+
end
|
|
98
|
+
|
|
58
99
|
def handle_interactive_shell_command(input, agent)
|
|
59
100
|
command = input.to_s.sub(/\A!\s*/, "")
|
|
60
101
|
if command.strip.empty?
|
|
@@ -73,6 +114,227 @@ module Kward
|
|
|
73
114
|
input.to_s.start_with?("!")
|
|
74
115
|
end
|
|
75
116
|
|
|
117
|
+
def run_ekwsh(agent)
|
|
118
|
+
unless @prompt.respond_to?(:ask)
|
|
119
|
+
runtime_output("The embedded shell is only available in interactive mode.")
|
|
120
|
+
return
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
tab = active_tab if respond_to?(:active_tab, true)
|
|
124
|
+
entering = tab.nil? || tab.shell.nil?
|
|
125
|
+
shell = tab&.shell || build_ekwsh(agent)
|
|
126
|
+
tab.shell = shell if tab
|
|
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, history: build_ekwsh_history(agent))
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def build_ekwsh(agent)
|
|
132
|
+
config = ConfigFiles.read_ekwsh_config
|
|
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
|
|
178
|
+
end
|
|
179
|
+
|
|
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)
|
|
194
|
+
loop do
|
|
195
|
+
if @prompt.respond_to?(:editing_file?) && @prompt.editing_file?
|
|
196
|
+
editor_result = @prompt.run_editor
|
|
197
|
+
if editor_result.is_a?(Hash) && editor_result[:tab_action]
|
|
198
|
+
(@pending_inputs ||= []).unshift(editor_result)
|
|
199
|
+
return :tab_action
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
input = ask_ekwsh(shell)
|
|
204
|
+
if input.is_a?(Hash) && input[:tab_action]
|
|
205
|
+
(@pending_inputs ||= []).unshift(input)
|
|
206
|
+
return :tab_action
|
|
207
|
+
end
|
|
208
|
+
break if input.nil?
|
|
209
|
+
|
|
210
|
+
result = run_ekwsh_command(shell, input)
|
|
211
|
+
@prompt.clear_transcript if result.clear && @prompt.respond_to?(:clear_transcript)
|
|
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
|
+
|
|
215
|
+
if result.open_editor_path
|
|
216
|
+
editor_result = open_ekwsh_editor(result.open_editor_path, shell)
|
|
217
|
+
return :tab_action if editor_result == :tab_action
|
|
218
|
+
|
|
219
|
+
next
|
|
220
|
+
end
|
|
221
|
+
if result.interactive_command
|
|
222
|
+
run_ekwsh_interactive_pty_command(shell, result)
|
|
223
|
+
next
|
|
224
|
+
end
|
|
225
|
+
if result.exit_shell
|
|
226
|
+
tab.shell = nil if tab
|
|
227
|
+
runtime_output("Shell exited.")
|
|
228
|
+
return :exited
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
tab.shell = nil if tab
|
|
232
|
+
runtime_output("Shell exited.")
|
|
233
|
+
:exited
|
|
234
|
+
end
|
|
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
|
+
|
|
244
|
+
def open_ekwsh_editor(path, shell)
|
|
245
|
+
unless @prompt.respond_to?(:edit_file)
|
|
246
|
+
runtime_output("Integrated editor is unavailable in this prompt.")
|
|
247
|
+
return false
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
result = @prompt.edit_file(path, base_dir: shell.cwd, allow_new: true)
|
|
251
|
+
if result.is_a?(Hash) && result[:tab_action]
|
|
252
|
+
(@pending_inputs ||= []).unshift(result)
|
|
253
|
+
return :tab_action
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
result
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def ask_ekwsh(shell)
|
|
260
|
+
provider = ->(input, cursor) { shell.complete(input, cursor) }
|
|
261
|
+
if @prompt.respond_to?(:with_completion_provider)
|
|
262
|
+
@prompt.with_completion_provider(provider, slash_overlay: false) { @prompt.ask(shell.prompt_label) }
|
|
263
|
+
else
|
|
264
|
+
@prompt.ask(shell.prompt_label)
|
|
265
|
+
end
|
|
266
|
+
end
|
|
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
|
+
|
|
279
|
+
def run_ekwsh_command(shell, input)
|
|
280
|
+
if @prompt.respond_to?(:begin_busy_input)
|
|
281
|
+
@prompt.begin_busy_input(shell.prompt_label, activity: "running")
|
|
282
|
+
end
|
|
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
|
|
290
|
+
ensure
|
|
291
|
+
@prompt.finish_busy_input if @prompt.respond_to?(:finish_busy_input)
|
|
292
|
+
end
|
|
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
|
+
|
|
76
338
|
def configured_workspace(root: current_workspace_root)
|
|
77
339
|
Workspace.new(root: root, guardrails: workspace_guardrails_enabled?)
|
|
78
340
|
end
|
|
@@ -146,10 +408,12 @@ module Kward
|
|
|
146
408
|
@client.reload_config if @client.respond_to?(:reload_config)
|
|
147
409
|
end
|
|
148
410
|
|
|
149
|
-
def refresh_conversation_runtime(conversation)
|
|
411
|
+
def refresh_conversation_runtime(conversation, reasoning_effort: current_reasoning_effort, refresh_system_message: true)
|
|
150
412
|
return unless conversation&.respond_to?(:update_runtime_context!)
|
|
151
413
|
|
|
152
|
-
conversation.
|
|
414
|
+
runtime_changed = [conversation.provider, conversation.model, conversation.reasoning_effort] != [current_model_provider, current_model_id, reasoning_effort]
|
|
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!)
|
|
153
417
|
update_assistant_prompt(conversation)
|
|
154
418
|
end
|
|
155
419
|
|
|
@@ -162,7 +426,7 @@ module Kward
|
|
|
162
426
|
end
|
|
163
427
|
|
|
164
428
|
def default_session_name(input)
|
|
165
|
-
|
|
429
|
+
SessionNaming.default_name(input)
|
|
166
430
|
end
|
|
167
431
|
|
|
168
432
|
end
|
data/lib/kward/cli/sessions.rb
CHANGED
|
@@ -12,7 +12,7 @@ module Kward
|
|
|
12
12
|
return @session_store if @session_store
|
|
13
13
|
return nil if agent
|
|
14
14
|
|
|
15
|
-
SessionStore.new
|
|
15
|
+
@session_store = SessionStore.new
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
def resume_last_session(session_store)
|
|
@@ -69,7 +69,7 @@ module Kward
|
|
|
69
69
|
end
|
|
70
70
|
|
|
71
71
|
def mutation_tool_call?(tool_call)
|
|
72
|
-
|
|
72
|
+
ToolCall.file_change_tool?(ToolCall.name(tool_call))
|
|
73
73
|
end
|
|
74
74
|
|
|
75
75
|
def cleanup_unused_sessions
|
data/lib/kward/cli/settings.rb
CHANGED
|
@@ -222,6 +222,21 @@ module Kward
|
|
|
222
222
|
when /\Ashow busy help/, /\Ahide busy help/
|
|
223
223
|
set_composer_busy_help(!composer_busy_help?)
|
|
224
224
|
runtime_output("Busy help #{composer_busy_help? ? "enabled" : "disabled"}. Restart the TUI to apply this setting.")
|
|
225
|
+
when /\Atab keybindings/
|
|
226
|
+
configure_tab_keybindings
|
|
227
|
+
when /\Aeditor mode/
|
|
228
|
+
configure_editor_mode
|
|
229
|
+
when /\Aeditor line numbers/
|
|
230
|
+
configure_editor_line_numbers
|
|
231
|
+
when /\Aenable auto-close pairs/, /\Adisable auto-close pairs/
|
|
232
|
+
set_editor_auto_close_pairs_enabled(!editor_auto_close_pairs_enabled?)
|
|
233
|
+
runtime_output("Editor auto-close pairs #{editor_auto_close_pairs_enabled? ? "enabled" : "disabled"}.")
|
|
234
|
+
when /\Aenable soft-wrap/, /\Adisable soft-wrap/
|
|
235
|
+
set_editor_soft_wrap_enabled(!editor_soft_wrap_enabled?)
|
|
236
|
+
runtime_output("Editor soft-wrap #{editor_soft_wrap_enabled? ? "enabled" : "disabled"}.")
|
|
237
|
+
when /\Aenable bar cursor/, /\Adisable bar cursor/
|
|
238
|
+
set_editor_bar_cursor_enabled(!editor_bar_cursor_enabled?)
|
|
239
|
+
runtime_output("Editor bar cursor #{editor_bar_cursor_enabled? ? "enabled" : "disabled"}.")
|
|
225
240
|
when /\Aenable session auto-resume/, /\Adisable session auto-resume/
|
|
226
241
|
set_session_auto_resume_enabled(!session_auto_resume_enabled?)
|
|
227
242
|
runtime_output("Session auto-resume #{session_auto_resume_enabled? ? "enabled" : "disabled"}.")
|
|
@@ -234,6 +249,12 @@ module Kward
|
|
|
234
249
|
"Overlay alignment (#{settings["alignment"]})",
|
|
235
250
|
"Overlay width (#{settings["width"]})",
|
|
236
251
|
"#{composer_busy_help? ? "Hide" : "Show"} busy help (currently #{on_off(composer_busy_help?)})",
|
|
252
|
+
"Tab keybindings (#{composer_tab_keybindings})",
|
|
253
|
+
"Editor mode (#{editor_mode})",
|
|
254
|
+
"Editor line numbers (#{editor_line_numbers})",
|
|
255
|
+
"#{editor_auto_close_pairs_enabled? ? "Disable" : "Enable"} auto-close pairs (currently #{on_off(editor_auto_close_pairs_enabled?)})",
|
|
256
|
+
"#{editor_soft_wrap_enabled? ? "Disable" : "Enable"} soft-wrap (currently #{on_off(editor_soft_wrap_enabled?)})",
|
|
257
|
+
"#{editor_bar_cursor_enabled? ? "Disable" : "Enable"} bar cursor (currently #{on_off(editor_bar_cursor_enabled?)})",
|
|
237
258
|
"#{session_auto_resume_enabled? ? "Disable" : "Enable"} session auto-resume (currently #{on_off(session_auto_resume_enabled?)})",
|
|
238
259
|
"Back"
|
|
239
260
|
]
|
|
@@ -243,6 +264,84 @@ module Kward
|
|
|
243
264
|
ConfigFiles.composer_busy_help?(safely_read_config.to_h)
|
|
244
265
|
end
|
|
245
266
|
|
|
267
|
+
def composer_tab_keybindings
|
|
268
|
+
ConfigFiles.composer_tab_keybindings(safely_read_config.to_h)
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def configure_tab_keybindings
|
|
272
|
+
selected = @prompt.select("Tab keybindings", tab_keybinding_choices, title: "Settings")
|
|
273
|
+
value = selected.to_s.split.first.to_s.downcase
|
|
274
|
+
return unless %w[auto ctrl alt].include?(value)
|
|
275
|
+
|
|
276
|
+
update_nested_config("composer", "tab_keybindings" => value)
|
|
277
|
+
runtime_output("Tab keybindings set to #{value}. Restart the TUI to apply this setting.")
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def tab_keybinding_choices
|
|
281
|
+
current = composer_tab_keybindings
|
|
282
|
+
%w[auto ctrl alt].map { |value| value == current ? "#{value} (current)" : value }
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def editor_mode
|
|
286
|
+
ConfigFiles.editor_mode(safely_read_config.to_h)
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def configure_editor_mode
|
|
290
|
+
selected = @prompt.select("Editor mode", editor_mode_choices, title: "Settings")
|
|
291
|
+
value = selected.to_s.split.first.to_s.downcase
|
|
292
|
+
return unless %w[modern emacs vibe].include?(value)
|
|
293
|
+
|
|
294
|
+
update_nested_config("editor", "mode" => value)
|
|
295
|
+
runtime_output("Editor mode set to #{value}. New editor buffers will use this mode.")
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def editor_mode_choices
|
|
299
|
+
current = editor_mode
|
|
300
|
+
%w[modern emacs vibe].map { |value| value == current ? "#{value} (current)" : value }
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def editor_line_numbers
|
|
304
|
+
ConfigFiles.editor_line_numbers(safely_read_config.to_h)
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def configure_editor_line_numbers
|
|
308
|
+
selected = @prompt.select("Editor line numbers", editor_line_number_choices, title: "Settings")
|
|
309
|
+
value = selected.to_s.split.first.to_s.downcase
|
|
310
|
+
return unless %w[absolute relative].include?(value)
|
|
311
|
+
|
|
312
|
+
update_nested_config("editor", "line_numbers" => value)
|
|
313
|
+
runtime_output("Editor line numbers set to #{value}.")
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def editor_line_number_choices
|
|
317
|
+
current = editor_line_numbers
|
|
318
|
+
%w[absolute relative].map { |value| value == current ? "#{value} (current)" : value }
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def editor_auto_close_pairs_enabled?
|
|
322
|
+
ConfigFiles.editor_auto_close_pairs?(safely_read_config.to_h)
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def set_editor_auto_close_pairs_enabled(enabled)
|
|
326
|
+
update_nested_config("editor", "auto_close_pairs" => enabled)
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def editor_soft_wrap_enabled?
|
|
330
|
+
ConfigFiles.editor_soft_wrap?(safely_read_config.to_h)
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def set_editor_soft_wrap_enabled(enabled)
|
|
334
|
+
update_nested_config("editor", "soft_wrap" => enabled)
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def editor_bar_cursor_enabled?
|
|
338
|
+
ConfigFiles.editor_bar_cursor?(safely_read_config.to_h)
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def set_editor_bar_cursor_enabled(enabled)
|
|
342
|
+
update_nested_config("editor", "bar_cursor" => enabled)
|
|
343
|
+
end
|
|
344
|
+
|
|
246
345
|
def session_auto_resume_enabled?
|
|
247
346
|
ConfigFiles.session_auto_resume_enabled?(safely_read_config.to_h)
|
|
248
347
|
end
|
|
@@ -455,11 +554,7 @@ module Kward
|
|
|
455
554
|
end
|
|
456
555
|
|
|
457
556
|
def update_nested_config(section, values)
|
|
458
|
-
|
|
459
|
-
current = config[section].is_a?(Hash) ? config[section].dup : {}
|
|
460
|
-
config[section] = current.merge(values)
|
|
461
|
-
ConfigFiles.write_config(config)
|
|
462
|
-
config
|
|
557
|
+
ConfigFiles.update_nested_config(section, values)
|
|
463
558
|
end
|
|
464
559
|
|
|
465
560
|
def on_off(value)
|
|
@@ -526,10 +621,7 @@ module Kward
|
|
|
526
621
|
effort, = choices.find { |_value, label| selected.to_s.downcase.start_with?(label.downcase) }
|
|
527
622
|
raise "Reasoning effort must be one of: #{choices.map(&:first).join(", ")}" unless effort
|
|
528
623
|
|
|
529
|
-
|
|
530
|
-
reload_client_config
|
|
531
|
-
refresh_conversation_runtime(conversation)
|
|
532
|
-
@prompt.redraw if @prompt.respond_to?(:redraw)
|
|
624
|
+
set_reasoning_effort(effort, conversation, provider: provider)
|
|
533
625
|
rescue StandardError => e
|
|
534
626
|
runtime_output("Reasoning error: #{e.message}")
|
|
535
627
|
end
|
|
@@ -609,6 +701,122 @@ module Kward
|
|
|
609
701
|
end
|
|
610
702
|
end
|
|
611
703
|
|
|
704
|
+
REASONING_CONFIG_DEBOUNCE_SECONDS = 0.5
|
|
705
|
+
|
|
706
|
+
def cycle_reasoning(conversation = current_footer_conversation, direction: :next, persist: :immediate)
|
|
707
|
+
provider = conversation&.provider || current_model_provider
|
|
708
|
+
model = conversation&.model || current_model_id
|
|
709
|
+
choices = ModelInfo.reasoning_effort_choices(provider, model)
|
|
710
|
+
return false if choices.empty?
|
|
711
|
+
|
|
712
|
+
current = (pending_reasoning_effort(provider) || conversation&.reasoning_effort || current_reasoning_effort).to_s
|
|
713
|
+
current_index = choices.index { |effort, _label| effort == current }
|
|
714
|
+
current_index ||= direction == :previous ? 0 : -1
|
|
715
|
+
offset = direction == :previous ? -1 : 1
|
|
716
|
+
effort = choices[(current_index + offset) % choices.length].first
|
|
717
|
+
persist == :debounced ? apply_reasoning_effort(effort, conversation, provider: provider) : set_reasoning_effort(effort, conversation, provider: provider)
|
|
718
|
+
true
|
|
719
|
+
rescue StandardError => e
|
|
720
|
+
runtime_output("Reasoning error: #{e.message}")
|
|
721
|
+
false
|
|
722
|
+
end
|
|
723
|
+
|
|
724
|
+
def set_reasoning_effort(effort, conversation = nil, provider: nil)
|
|
725
|
+
@pending_reasoning_config_mutex.synchronize { @pending_reasoning_config = nil }
|
|
726
|
+
persist_reasoning_config(effort, provider: provider)
|
|
727
|
+
apply_reasoning_effort(effort, conversation, provider: provider, queue_config: false)
|
|
728
|
+
end
|
|
729
|
+
|
|
730
|
+
def apply_reasoning_effort(effort, conversation = nil, provider: nil, queue_config: true)
|
|
731
|
+
queue_reasoning_config(effort, provider: provider, conversation: conversation) if queue_config
|
|
732
|
+
if queue_config
|
|
733
|
+
update_conversation_reasoning_effort(conversation, effort)
|
|
734
|
+
refresh_reasoning_status
|
|
735
|
+
else
|
|
736
|
+
refresh_conversation_runtime(conversation, reasoning_effort: effort)
|
|
737
|
+
@prompt.redraw if @prompt.respond_to?(:redraw)
|
|
738
|
+
end
|
|
739
|
+
end
|
|
740
|
+
|
|
741
|
+
def refresh_reasoning_status
|
|
742
|
+
if @prompt.respond_to?(:refresh_composer_status)
|
|
743
|
+
@prompt.refresh_composer_status
|
|
744
|
+
else
|
|
745
|
+
@prompt.redraw if @prompt.respond_to?(:redraw)
|
|
746
|
+
end
|
|
747
|
+
end
|
|
748
|
+
|
|
749
|
+
def update_conversation_reasoning_effort(conversation, effort)
|
|
750
|
+
return unless conversation&.respond_to?(:update_runtime_context!)
|
|
751
|
+
|
|
752
|
+
conversation.update_runtime_context!(
|
|
753
|
+
provider: conversation.provider || current_model_provider,
|
|
754
|
+
model: conversation.model || current_model_id,
|
|
755
|
+
reasoning_effort: effort,
|
|
756
|
+
refresh: false
|
|
757
|
+
)
|
|
758
|
+
end
|
|
759
|
+
|
|
760
|
+
def pending_reasoning_effort(provider)
|
|
761
|
+
@pending_reasoning_config_mutex.synchronize do
|
|
762
|
+
pending = @pending_reasoning_config
|
|
763
|
+
return nil unless pending
|
|
764
|
+
return nil unless pending[:provider].to_s.downcase == provider.to_s.downcase
|
|
765
|
+
|
|
766
|
+
pending[:effort]
|
|
767
|
+
end
|
|
768
|
+
end
|
|
769
|
+
|
|
770
|
+
def queue_reasoning_config(effort, provider: nil, conversation: nil)
|
|
771
|
+
pending = {
|
|
772
|
+
effort: effort,
|
|
773
|
+
provider: provider || current_model_provider,
|
|
774
|
+
conversation: conversation,
|
|
775
|
+
deadline: Process.clock_gettime(Process::CLOCK_MONOTONIC) + REASONING_CONFIG_DEBOUNCE_SECONDS
|
|
776
|
+
}
|
|
777
|
+
@pending_reasoning_config_mutex.synchronize { @pending_reasoning_config = pending }
|
|
778
|
+
schedule_reasoning_config_flush
|
|
779
|
+
end
|
|
780
|
+
|
|
781
|
+
def schedule_reasoning_config_flush
|
|
782
|
+
return if @pending_reasoning_config_thread&.alive?
|
|
783
|
+
|
|
784
|
+
@pending_reasoning_config_thread = Thread.new do
|
|
785
|
+
loop do
|
|
786
|
+
sleep REASONING_CONFIG_DEBOUNCE_SECONDS
|
|
787
|
+
break if flush_pending_reasoning_config(force: false)
|
|
788
|
+
break unless @pending_reasoning_config_mutex.synchronize { @pending_reasoning_config }
|
|
789
|
+
end
|
|
790
|
+
rescue StandardError => e
|
|
791
|
+
runtime_output("Reasoning error: #{e.message}")
|
|
792
|
+
end
|
|
793
|
+
end
|
|
794
|
+
|
|
795
|
+
def flush_pending_reasoning_config(force: true, conversation: nil)
|
|
796
|
+
pending = nil
|
|
797
|
+
@pending_reasoning_config_mutex.synchronize do
|
|
798
|
+
pending = @pending_reasoning_config
|
|
799
|
+
return false unless pending
|
|
800
|
+
|
|
801
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
802
|
+
return false if !force && now < pending[:deadline].to_f
|
|
803
|
+
|
|
804
|
+
@pending_reasoning_config = nil
|
|
805
|
+
end
|
|
806
|
+
persist_reasoning_config(pending[:effort], provider: pending[:provider])
|
|
807
|
+
conversation ||= pending[:conversation]
|
|
808
|
+
if conversation&.reasoning_effort.to_s == pending[:effort].to_s
|
|
809
|
+
refresh_conversation_runtime(conversation, reasoning_effort: pending[:effort])
|
|
810
|
+
conversation.persist_runtime_context! if conversation.respond_to?(:persist_runtime_context!)
|
|
811
|
+
end
|
|
812
|
+
true
|
|
813
|
+
end
|
|
814
|
+
|
|
815
|
+
def persist_reasoning_config(effort, provider: nil)
|
|
816
|
+
ConfigFiles.update_config(ModelInfo.reasoning_config_key_for_provider(provider || current_model_provider) => effort)
|
|
817
|
+
reload_client_config
|
|
818
|
+
end
|
|
819
|
+
|
|
612
820
|
def reasoning_choices(choices, conversation = current_footer_conversation)
|
|
613
821
|
current = (conversation.reasoning_effort || (@client.respond_to?(:current_reasoning_effort) ? @client.current_reasoning_effort : ModelInfo::DEFAULT_REASONING_EFFORT)).to_s
|
|
614
822
|
choices.map do |effort, label|
|