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
|
@@ -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,95 @@ 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)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def build_ekwsh(agent)
|
|
132
|
+
config = ConfigFiles.read_ekwsh_config
|
|
133
|
+
Ekwsh.new(cwd: interactive_workspace_root(agent), configured_env: config[:env], aliases: config[:aliases])
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def run_ekwsh_loop(shell, tab: nil)
|
|
137
|
+
loop do
|
|
138
|
+
if @prompt.respond_to?(:editing_file?) && @prompt.editing_file?
|
|
139
|
+
editor_result = @prompt.run_editor
|
|
140
|
+
if editor_result.is_a?(Hash) && editor_result[:tab_action]
|
|
141
|
+
(@pending_inputs ||= []).unshift(editor_result)
|
|
142
|
+
return :tab_action
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
input = ask_ekwsh(shell)
|
|
147
|
+
if input.is_a?(Hash) && input[:tab_action]
|
|
148
|
+
(@pending_inputs ||= []).unshift(input)
|
|
149
|
+
return :tab_action
|
|
150
|
+
end
|
|
151
|
+
break if input.nil?
|
|
152
|
+
|
|
153
|
+
result = run_ekwsh_command(shell, input)
|
|
154
|
+
@prompt.clear_transcript if result.clear && @prompt.respond_to?(:clear_transcript)
|
|
155
|
+
@prompt.say(result.output) unless result.output.to_s.empty?
|
|
156
|
+
if result.open_editor_path
|
|
157
|
+
editor_result = open_ekwsh_editor(result.open_editor_path, shell)
|
|
158
|
+
return :tab_action if editor_result == :tab_action
|
|
159
|
+
|
|
160
|
+
next
|
|
161
|
+
end
|
|
162
|
+
if result.exit_shell
|
|
163
|
+
tab.shell = nil if tab
|
|
164
|
+
runtime_output("Shell exited.")
|
|
165
|
+
return :exited
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
tab.shell = nil if tab
|
|
169
|
+
runtime_output("Shell exited.")
|
|
170
|
+
:exited
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def open_ekwsh_editor(path, shell)
|
|
174
|
+
unless @prompt.respond_to?(:edit_file)
|
|
175
|
+
runtime_output("Integrated editor is unavailable in this prompt.")
|
|
176
|
+
return false
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
result = @prompt.edit_file(path, base_dir: shell.cwd, allow_new: true)
|
|
180
|
+
if result.is_a?(Hash) && result[:tab_action]
|
|
181
|
+
(@pending_inputs ||= []).unshift(result)
|
|
182
|
+
return :tab_action
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
result
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def ask_ekwsh(shell)
|
|
189
|
+
provider = ->(input, cursor) { shell.complete(input, cursor) }
|
|
190
|
+
if @prompt.respond_to?(:with_completion_provider)
|
|
191
|
+
@prompt.with_completion_provider(provider) { @prompt.ask(shell.prompt_label) }
|
|
192
|
+
else
|
|
193
|
+
@prompt.ask(shell.prompt_label)
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def run_ekwsh_command(shell, input)
|
|
198
|
+
if @prompt.respond_to?(:begin_busy_input)
|
|
199
|
+
@prompt.begin_busy_input(shell.prompt_label, activity: "running")
|
|
200
|
+
end
|
|
201
|
+
shell.run(input)
|
|
202
|
+
ensure
|
|
203
|
+
@prompt.finish_busy_input if @prompt.respond_to?(:finish_busy_input)
|
|
204
|
+
end
|
|
205
|
+
|
|
76
206
|
def configured_workspace(root: current_workspace_root)
|
|
77
207
|
Workspace.new(root: root, guardrails: workspace_guardrails_enabled?)
|
|
78
208
|
end
|
|
@@ -146,10 +276,10 @@ module Kward
|
|
|
146
276
|
@client.reload_config if @client.respond_to?(:reload_config)
|
|
147
277
|
end
|
|
148
278
|
|
|
149
|
-
def refresh_conversation_runtime(conversation)
|
|
279
|
+
def refresh_conversation_runtime(conversation, reasoning_effort: current_reasoning_effort, refresh_system_message: true)
|
|
150
280
|
return unless conversation&.respond_to?(:update_runtime_context!)
|
|
151
281
|
|
|
152
|
-
conversation.update_runtime_context!(provider: current_model_provider, model: current_model_id, reasoning_effort:
|
|
282
|
+
conversation.update_runtime_context!(provider: current_model_provider, model: current_model_id, reasoning_effort: reasoning_effort, refresh: refresh_system_message)
|
|
153
283
|
update_assistant_prompt(conversation)
|
|
154
284
|
end
|
|
155
285
|
|
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,123 @@ 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
|
+
conversation.persist_runtime_context! if conversation&.respond_to?(:persist_runtime_context!)
|
|
738
|
+
@prompt.redraw if @prompt.respond_to?(:redraw)
|
|
739
|
+
end
|
|
740
|
+
end
|
|
741
|
+
|
|
742
|
+
def refresh_reasoning_status
|
|
743
|
+
if @prompt.respond_to?(:refresh_composer_status)
|
|
744
|
+
@prompt.refresh_composer_status
|
|
745
|
+
else
|
|
746
|
+
@prompt.redraw if @prompt.respond_to?(:redraw)
|
|
747
|
+
end
|
|
748
|
+
end
|
|
749
|
+
|
|
750
|
+
def update_conversation_reasoning_effort(conversation, effort)
|
|
751
|
+
return unless conversation&.respond_to?(:update_runtime_context!)
|
|
752
|
+
|
|
753
|
+
conversation.update_runtime_context!(
|
|
754
|
+
provider: conversation.provider || current_model_provider,
|
|
755
|
+
model: conversation.model || current_model_id,
|
|
756
|
+
reasoning_effort: effort,
|
|
757
|
+
refresh: false
|
|
758
|
+
)
|
|
759
|
+
end
|
|
760
|
+
|
|
761
|
+
def pending_reasoning_effort(provider)
|
|
762
|
+
@pending_reasoning_config_mutex.synchronize do
|
|
763
|
+
pending = @pending_reasoning_config
|
|
764
|
+
return nil unless pending
|
|
765
|
+
return nil unless pending[:provider].to_s.downcase == provider.to_s.downcase
|
|
766
|
+
|
|
767
|
+
pending[:effort]
|
|
768
|
+
end
|
|
769
|
+
end
|
|
770
|
+
|
|
771
|
+
def queue_reasoning_config(effort, provider: nil, conversation: nil)
|
|
772
|
+
pending = {
|
|
773
|
+
effort: effort,
|
|
774
|
+
provider: provider || current_model_provider,
|
|
775
|
+
conversation: conversation,
|
|
776
|
+
deadline: Process.clock_gettime(Process::CLOCK_MONOTONIC) + REASONING_CONFIG_DEBOUNCE_SECONDS
|
|
777
|
+
}
|
|
778
|
+
@pending_reasoning_config_mutex.synchronize { @pending_reasoning_config = pending }
|
|
779
|
+
schedule_reasoning_config_flush
|
|
780
|
+
end
|
|
781
|
+
|
|
782
|
+
def schedule_reasoning_config_flush
|
|
783
|
+
return if @pending_reasoning_config_thread&.alive?
|
|
784
|
+
|
|
785
|
+
@pending_reasoning_config_thread = Thread.new do
|
|
786
|
+
loop do
|
|
787
|
+
sleep REASONING_CONFIG_DEBOUNCE_SECONDS
|
|
788
|
+
break if flush_pending_reasoning_config(force: false)
|
|
789
|
+
break unless @pending_reasoning_config_mutex.synchronize { @pending_reasoning_config }
|
|
790
|
+
end
|
|
791
|
+
rescue StandardError => e
|
|
792
|
+
runtime_output("Reasoning error: #{e.message}")
|
|
793
|
+
end
|
|
794
|
+
end
|
|
795
|
+
|
|
796
|
+
def flush_pending_reasoning_config(force: true, conversation: nil)
|
|
797
|
+
pending = nil
|
|
798
|
+
@pending_reasoning_config_mutex.synchronize do
|
|
799
|
+
pending = @pending_reasoning_config
|
|
800
|
+
return false unless pending
|
|
801
|
+
|
|
802
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
803
|
+
return false if !force && now < pending[:deadline].to_f
|
|
804
|
+
|
|
805
|
+
@pending_reasoning_config = nil
|
|
806
|
+
end
|
|
807
|
+
persist_reasoning_config(pending[:effort], provider: pending[:provider])
|
|
808
|
+
conversation ||= pending[:conversation]
|
|
809
|
+
if conversation&.reasoning_effort.to_s == pending[:effort].to_s
|
|
810
|
+
refresh_conversation_runtime(conversation, reasoning_effort: pending[:effort])
|
|
811
|
+
conversation.persist_runtime_context! if conversation.respond_to?(:persist_runtime_context!)
|
|
812
|
+
end
|
|
813
|
+
true
|
|
814
|
+
end
|
|
815
|
+
|
|
816
|
+
def persist_reasoning_config(effort, provider: nil)
|
|
817
|
+
ConfigFiles.update_config(ModelInfo.reasoning_config_key_for_provider(provider || current_model_provider) => effort)
|
|
818
|
+
reload_client_config
|
|
819
|
+
end
|
|
820
|
+
|
|
612
821
|
def reasoning_choices(choices, conversation = current_footer_conversation)
|
|
613
822
|
current = (conversation.reasoning_effort || (@client.respond_to?(:current_reasoning_effort) ? @client.current_reasoning_effort : ModelInfo::DEFAULT_REASONING_EFFORT)).to_s
|
|
614
823
|
choices.map do |effort, label|
|