kward 0.84.0 → 0.85.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 +34 -0
- data/Gemfile.lock +2 -2
- data/README.md +2 -2
- data/doc/agent-tools.md +13 -1
- data/doc/api.md +17 -2
- data/doc/composer.md +1 -1
- data/doc/configuration.md +27 -4
- data/doc/extensibility.md +2 -1
- data/doc/git.md +3 -1
- data/doc/pan.md +6 -0
- data/doc/permissions.md +4 -4
- data/doc/plugins.md +464 -15
- data/doc/rpc.md +154 -16
- data/doc/sandboxing.md +11 -5
- data/doc/security.md +7 -1
- data/doc/session-management.md +5 -4
- data/doc/tabs.md +6 -2
- data/doc/transports.md +15 -0
- data/doc/usage.md +4 -1
- data/doc/workspace-tools.md +9 -0
- data/examples/plugins/space_invaders.rb +1 -1
- data/examples/plugins/stardate_footer.rb +2 -2
- data/examples/plugins/telegram/plugin.rb +1 -1
- data/lib/kward/agent.rb +24 -11
- data/lib/kward/cli/compaction.rb +9 -3
- data/lib/kward/cli/plugins.rb +81 -12
- data/lib/kward/cli/prompt_interface.rb +25 -5
- data/lib/kward/cli/rendering.rb +3 -0
- data/lib/kward/cli/runtime_helpers.rb +121 -27
- data/lib/kward/cli/sessions.rb +9 -5
- data/lib/kward/cli/settings/menus.rb +745 -0
- data/lib/kward/cli/settings/model.rb +327 -0
- data/lib/kward/cli/settings.rb +6 -1055
- data/lib/kward/cli/slash_commands.rb +45 -4
- data/lib/kward/cli/tabs.rb +167 -27
- data/lib/kward/{cli_transcript_formatter.rb → cli/transcript_formatter.rb} +3 -3
- data/lib/kward/cli/worktrees.rb +65 -2
- data/lib/kward/cli.rb +26 -24
- data/lib/kward/compactor.rb +18 -7
- data/lib/kward/config/core.rb +389 -0
- data/lib/kward/config/extensions.rb +96 -0
- data/lib/kward/config/prompts.rb +313 -0
- data/lib/kward/config/settings.rb +250 -0
- data/lib/kward/config_files.rb +14 -1008
- data/lib/kward/conversation.rb +31 -2
- data/lib/kward/image_attachments.rb +1 -1
- data/lib/kward/model/client.rb +2 -2
- data/lib/kward/model/copilot_models.rb +2 -2
- data/lib/kward/model/model_info.rb +20 -3
- data/lib/kward/{openrouter_model_cache.rb → model/openrouter_model_cache.rb} +3 -3
- data/lib/kward/model/payloads.rb +12 -3
- data/lib/kward/model/typesafe_client.rb +78 -0
- data/lib/kward/pan/server.rb +10 -7
- data/lib/kward/permissions/policy.rb +6 -2
- data/lib/kward/plugin_registry.rb +2 -659
- data/lib/kward/plugins/actions.rb +453 -0
- data/lib/kward/plugins/chat_contract.rb +121 -0
- data/lib/kward/{plugin_chat_runtime.rb → plugins/chat_runtime.rb} +62 -18
- data/lib/kward/plugins/host.rb +232 -0
- data/lib/kward/plugins/registry.rb +1190 -0
- data/lib/kward/plugins/resources.rb +206 -0
- data/lib/kward/plugins/turn_request.rb +36 -0
- data/lib/kward/plugins/ui.rb +219 -0
- data/lib/kward/prompt_interface/composer_renderer.rb +1 -1
- data/lib/kward/prompt_interface/composer_state.rb +1 -1
- data/lib/kward/prompt_interface/editor/controller.rb +1 -1
- data/lib/kward/prompt_interface/editor/runner.rb +1 -1
- data/lib/kward/prompt_interface/editor/state.rb +1 -1
- data/lib/kward/prompt_interface/layout.rb +1 -1
- data/lib/kward/prompt_interface/overlay_renderer.rb +1 -1
- data/lib/kward/prompt_interface/plugin_ui_requests.rb +82 -0
- data/lib/kward/prompt_interface/runtime_state.rb +6 -1
- data/lib/kward/prompt_interface/screen.rb +9 -2
- data/lib/kward/prompt_interface.rb +60 -11
- data/lib/kward/prompts/commands.rb +2 -1
- data/lib/kward/{adaptive_pty_output_sink.rb → pty/adaptive_output_sink.rb} +1 -1
- data/lib/kward/{interactive_pty_runner.rb → pty/interactive_runner.rb} +2 -2
- data/lib/kward/{local_command_runner.rb → pty/local_command_runner.rb} +1 -1
- data/lib/kward/{local_pty_command_runner.rb → pty/local_pty_runner.rb} +3 -9
- data/lib/kward/{pty_output_sink.rb → pty/output_sink.rb} +9 -4
- data/lib/kward/{pty_transcript_normalizer.rb → pty/transcript_normalizer.rb} +1 -1
- data/lib/kward/rpc/plugin_chat_manager.rb +30 -10
- data/lib/kward/rpc/prompt_bridge.rb +25 -0
- data/lib/kward/rpc/server.rb +91 -12
- data/lib/kward/rpc/session_manager.rb +147 -44
- data/lib/kward/rpc/session_tree_rows.rb +2 -2
- data/lib/kward/rpc/tool_metadata.rb +1 -1
- data/lib/kward/sandbox/command_runner.rb +1 -1
- data/lib/kward/{session_catalog.rb → sessions/catalog.rb} +1 -1
- data/lib/kward/{session_store.rb → sessions/store.rb} +9 -9
- data/lib/kward/{session_tree_nodes.rb → sessions/tree_nodes.rb} +3 -3
- data/lib/kward/{session_tree_renderer.rb → sessions/tree_renderer.rb} +4 -4
- data/lib/kward/{session_tree_tool_display.rb → sessions/tree_tool_display.rb} +1 -1
- data/lib/kward/{kwsh.rb → shell/kwsh.rb} +3 -3
- data/lib/kward/{persistent_shell_session.rb → shell/persistent_session.rb} +3 -3
- data/lib/kward/{shell_prompt_session.rb → shell/prompt_session.rb} +1 -1
- data/lib/kward/skills/trust_store.rb +1 -1
- data/lib/kward/tabs/driver.rb +194 -0
- data/lib/kward/{tab_store.rb → tabs/store.rb} +2 -2
- data/lib/kward/{clipboard.rb → terminal/clipboard.rb} +1 -1
- data/lib/kward/{terminal_image_support.rb → terminal/image_support.rb} +1 -1
- data/lib/kward/tools/base.rb +18 -0
- data/lib/kward/tools/context_for_task.rb +15 -6
- data/lib/kward/tools/edit_file.rb +9 -6
- data/lib/kward/tools/git_commit.rb +13 -7
- data/lib/kward/tools/list_directory.rb +4 -4
- data/lib/kward/tools/plugin_tool.rb +41 -0
- data/lib/kward/tools/prepare_shell_command.rb +1 -1
- data/lib/kward/tools/read_file.rb +7 -6
- data/lib/kward/tools/registry.rb +96 -13
- data/lib/kward/tools/run_shell_command.rb +10 -8
- data/lib/kward/tools/search/code.rb +1 -1
- data/lib/kward/tools/summarize_file_structure.rb +5 -5
- data/lib/kward/tools/tool_call.rb +2 -1
- data/lib/kward/tools/typesafe_evaluate.rb +81 -0
- data/lib/kward/tools/workspace_targets.rb +58 -0
- data/lib/kward/tools/write_file.rb +9 -6
- data/lib/kward/{export_path.rb → transcripts/export_path.rb} +1 -1
- data/lib/kward/{markdown_transcript.rb → transcripts/markdown_transcript.rb} +2 -2
- data/lib/kward/transport/contracts.rb +200 -0
- data/lib/kward/transport/gateway.rb +79 -35
- data/lib/kward/transport/plugin_chat_gateway.rb +3 -2
- data/lib/kward/transport.rb +1 -200
- data/lib/kward/version.rb +1 -1
- data/lib/kward/{workspace_factory.rb → workspace/factory.rb} +2 -2
- data/lib/kward/{git_worktree_manager.rb → workspace/git_worktree_manager.rb} +28 -0
- data/lib/kward/{workspace.rb → workspace/workspace.rb} +3 -3
- data/templates/default/fulldoc/html/css/kward.css +125 -0
- data/templates/default/fulldoc/html/images/kward_screen_1.png +0 -0
- data/templates/default/fulldoc/html/setup.rb +1 -1
- data/templates/default/layout/html/layout.erb +16 -4
- metadata +67 -48
- data/lib/kward/tab_driver.rb +0 -90
- data/templates/default/fulldoc/html/images/kward_workflow.svg +0 -52
- /data/lib/kward/{editor_prompt.rb → cli/editor_prompt.rb} +0 -0
- /data/lib/kward/{editor_prompt_session.rb → cli/editor_prompt_session.rb} +0 -0
- /data/lib/kward/{diff_view_mode.rb → prompt_interface/editor/diff_view_mode.rb} +0 -0
- /data/lib/kward/{editor_mode.rb → prompt_interface/editor/editor_mode.rb} +0 -0
- /data/lib/kward/{markdown_code_block.rb → prompt_interface/editor/markdown_code_block.rb} +0 -0
- /data/lib/kward/{scratchpad_languages.rb → prompt_interface/editor/scratchpad_languages.rb} +0 -0
- /data/lib/kward/{scratchpad_runner.rb → prompt_interface/editor/scratchpad_runner.rb} +0 -0
- /data/lib/kward/{detached_run.rb → pty/detached_run.rb} +0 -0
- /data/lib/kward/{session_diff.rb → sessions/diff.rb} +0 -0
- /data/lib/kward/{session_naming.rb → sessions/naming.rb} +0 -0
- /data/lib/kward/{session_trash.rb → sessions/trash.rb} +0 -0
- /data/lib/kward/{kwshrc.rb → shell/kwshrc.rb} +0 -0
- /data/lib/kward/{shell_prompt.rb → shell/prompt.rb} +0 -0
- /data/lib/kward/{ansi.rb → terminal/ansi.rb} +0 -0
- /data/lib/kward/{terminal_keys.rb → terminal/keys.rb} +0 -0
- /data/lib/kward/{terminal_sequences.rb → terminal/sequences.rb} +0 -0
- /data/lib/kward/{terminal_text.rb → terminal/text.rb} +0 -0
- /data/lib/kward/{transcript_export.rb → transcripts/transcript_export.rb} +0 -0
- /data/lib/kward/{project_files.rb → workspace/files.rb} +0 -0
- /data/lib/kward/{path_guard.rb → workspace/path_guard.rb} +0 -0
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
# CLI settings model behavior.
|
|
2
|
+
module Kward
|
|
3
|
+
class CLI
|
|
4
|
+
module Settings
|
|
5
|
+
module Model
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def configure_model(conversation = nil, models: nil)
|
|
9
|
+
unless model_overlay_available?
|
|
10
|
+
runtime_output("Model overlay is unavailable in this prompt.")
|
|
11
|
+
return
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
models ||= normalized_available_models(conversation)
|
|
15
|
+
provider_filter = nil
|
|
16
|
+
show_all = false
|
|
17
|
+
loop do
|
|
18
|
+
visible_models = provider_filter ? models.select { |model| model[:provider] == provider_filter } : models
|
|
19
|
+
visible_models = visible_models.select { |model| default_selectable_model?(model) } unless show_all
|
|
20
|
+
actions = ["Refresh model list", "Enter model ID manually", "Show all models", "Change provider"]
|
|
21
|
+
selected = @prompt.select("Default model", model_choices(visible_models, conversation) + actions, title: "Models", custom: true)
|
|
22
|
+
return unless selected
|
|
23
|
+
|
|
24
|
+
case selected.to_s
|
|
25
|
+
when "Refresh model list"
|
|
26
|
+
refreshed = @client.refresh_available_models(provider: provider_filter) if @client.respond_to?(:refresh_available_models)
|
|
27
|
+
models = normalized_available_models(conversation, models: refreshed)
|
|
28
|
+
when "Enter model ID manually"
|
|
29
|
+
provider = provider_filter || conversation&.provider || current_model_provider
|
|
30
|
+
model = @prompt.select("Model ID for #{provider}", [], title: "Models", custom: true)
|
|
31
|
+
return unless model
|
|
32
|
+
persist_model_selection(provider, model, conversation)
|
|
33
|
+
return
|
|
34
|
+
when "Show all models"
|
|
35
|
+
provider_filter = nil
|
|
36
|
+
show_all = true
|
|
37
|
+
when "Change provider"
|
|
38
|
+
selected_provider_name = selected_provider(@prompt.select("Provider", provider_choices, title: "Models"))
|
|
39
|
+
if selected_provider_name
|
|
40
|
+
provider_filter = selected_provider_name
|
|
41
|
+
show_all = false
|
|
42
|
+
end
|
|
43
|
+
else
|
|
44
|
+
provider, model = selected_model(selected, models)
|
|
45
|
+
persist_model_selection(provider, model, conversation)
|
|
46
|
+
return
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
rescue StandardError => e
|
|
50
|
+
runtime_output("Model error: #{e.message}")
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def configure_reasoning(conversation = nil)
|
|
54
|
+
unless model_overlay_available?
|
|
55
|
+
runtime_output("Reasoning overlay is unavailable in this prompt.")
|
|
56
|
+
return
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
provider = conversation&.provider || current_model_provider
|
|
60
|
+
model = conversation&.model || current_model_id
|
|
61
|
+
choices = ModelInfo.reasoning_effort_choices(provider, model)
|
|
62
|
+
if choices.empty?
|
|
63
|
+
runtime_output("Reasoning effort is unavailable for #{provider} #{model}.")
|
|
64
|
+
return
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
selected = @prompt.select("Reasoning effort", reasoning_choices(choices, conversation), title: "Reasoning")
|
|
68
|
+
return unless selected
|
|
69
|
+
|
|
70
|
+
effort, = choices.find { |_value, label| selected.to_s.downcase.start_with?(label.downcase) }
|
|
71
|
+
raise "Reasoning effort must be one of: #{choices.map(&:first).join(", ")}" unless effort
|
|
72
|
+
|
|
73
|
+
set_reasoning_effort(effort, conversation, provider: provider)
|
|
74
|
+
rescue StandardError => e
|
|
75
|
+
runtime_output("Reasoning error: #{e.message}")
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def login_picker_available?
|
|
79
|
+
@prompt.respond_to?(:select)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def login_method_choices
|
|
83
|
+
["API key", "Subscription / OAuth"]
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def selected_login_method(selected)
|
|
87
|
+
case selected.to_s.downcase
|
|
88
|
+
when /\Aapi key\z/ then :api_key
|
|
89
|
+
when /\Asubscription \/ oauth\z/ then :oauth
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def login_provider_prompt(method)
|
|
94
|
+
method == :api_key ? "Add an API key" : "Sign in with a subscription"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def login_provider_choices(method)
|
|
98
|
+
if method == :api_key
|
|
99
|
+
ProviderCatalog.api_key_providers.map(&:name)
|
|
100
|
+
else
|
|
101
|
+
["Anthropic Claude", "ChatGPT", "GitHub Copilot"]
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def selected_login_provider(selected)
|
|
106
|
+
value = selected.to_s.downcase
|
|
107
|
+
return "openai" if value == "openai" || value == "chatgpt"
|
|
108
|
+
return "anthropic" if value == "anthropic" || value == "anthropic claude"
|
|
109
|
+
return "copilot" if value == "github copilot"
|
|
110
|
+
|
|
111
|
+
ProviderCatalog.find_by_name(selected)&.id
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def model_overlay_available?
|
|
115
|
+
@prompt.respond_to?(:select)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def settings_overlay_available?
|
|
119
|
+
@prompt.respond_to?(:select)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def update_overlay_settings(values)
|
|
123
|
+
settings = ConfigFiles.update_overlay_settings(values)
|
|
124
|
+
@prompt.update_overlay_settings(settings) if @prompt.respond_to?(:update_overlay_settings)
|
|
125
|
+
settings
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def choose_overlay_setting(message, choices, values)
|
|
129
|
+
choice = @prompt.select(message, choices, title: "Settings")
|
|
130
|
+
return nil unless choice
|
|
131
|
+
|
|
132
|
+
values.find { |value| choice.to_s.downcase.start_with?(value) }
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def normalized_available_models(conversation = current_footer_conversation, models: nil)
|
|
136
|
+
current_provider = conversation.provider || (@client.respond_to?(:current_provider) ? @client.current_provider : "Codex")
|
|
137
|
+
current_model = conversation.model || (@client.respond_to?(:current_model) ? @client.current_model : nil)
|
|
138
|
+
current_reasoning = conversation.reasoning_effort || (@client.respond_to?(:current_reasoning_effort) ? @client.current_reasoning_effort : nil)
|
|
139
|
+
models ||= @client.respond_to?(:available_models) ? @client.available_models : []
|
|
140
|
+
ModelInfo.normalize_available(
|
|
141
|
+
models,
|
|
142
|
+
current_provider: current_provider,
|
|
143
|
+
current_model: current_model,
|
|
144
|
+
current_reasoning_effort: current_reasoning
|
|
145
|
+
)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def model_choices(models, conversation = current_footer_conversation)
|
|
149
|
+
current_provider = conversation.provider || current_model_provider
|
|
150
|
+
current_model = conversation.model || current_model_id
|
|
151
|
+
choices = models.map do |model|
|
|
152
|
+
label = "#{model[:provider]} #{model[:id]}"
|
|
153
|
+
label += " (current)" if model[:current]
|
|
154
|
+
label
|
|
155
|
+
end
|
|
156
|
+
choices.empty? ? ["#{current_provider} #{current_model} (current)"] : choices.uniq
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def default_selectable_model?(model)
|
|
160
|
+
return true if model[:current]
|
|
161
|
+
|
|
162
|
+
capabilities = Array(model[:supportedParameters]).map { |value| value.to_s.downcase }
|
|
163
|
+
return true if capabilities.empty?
|
|
164
|
+
|
|
165
|
+
capabilities.any? do |capability|
|
|
166
|
+
["tools", "toolchoice", "functioncalling", "generatecontent", "streamgeneratecontent"].include?(capability.delete("_-"))
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def selected_model(selected, models)
|
|
171
|
+
text = selected.to_s.sub(/ \(current\)\z/, "").strip
|
|
172
|
+
known = models.find { |model| "#{model[:provider]} #{model[:id]}" == text }
|
|
173
|
+
return [known[:provider], known[:id]] if known
|
|
174
|
+
|
|
175
|
+
provider, model = text.split(/\s+/, 2)
|
|
176
|
+
known_provider = ["Codex", "Anthropic", "OpenRouter", "Copilot", "Local"].include?(provider) || ProviderCatalog.find_by_name(provider)
|
|
177
|
+
return [provider, model.strip] if known_provider && !model.to_s.strip.empty?
|
|
178
|
+
|
|
179
|
+
[current_model_provider, text]
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def persist_model_selection(provider, model, conversation)
|
|
183
|
+
model = model.to_s.strip
|
|
184
|
+
raise "Model must be a non-empty string" if model.empty?
|
|
185
|
+
|
|
186
|
+
ConfigFiles.update_config(ModelInfo.config_values_for_selection(provider, model))
|
|
187
|
+
reload_client_config
|
|
188
|
+
refresh_conversation_runtime(conversation)
|
|
189
|
+
refresh_composer_status
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
REASONING_CONFIG_DEBOUNCE_SECONDS = 0.5
|
|
193
|
+
|
|
194
|
+
def cycle_reasoning(conversation = current_footer_conversation, direction: :next, persist: :immediate)
|
|
195
|
+
provider = conversation&.provider || current_model_provider
|
|
196
|
+
model = conversation&.model || current_model_id
|
|
197
|
+
choices = ModelInfo.reasoning_effort_choices(provider, model)
|
|
198
|
+
return false if choices.empty?
|
|
199
|
+
|
|
200
|
+
current = (pending_reasoning_effort(provider) || conversation&.reasoning_effort || current_reasoning_effort).to_s
|
|
201
|
+
current_index = choices.index { |effort, _label| effort == current }
|
|
202
|
+
current_index ||= direction == :previous ? 0 : -1
|
|
203
|
+
offset = direction == :previous ? -1 : 1
|
|
204
|
+
effort = choices[(current_index + offset) % choices.length].first
|
|
205
|
+
persist == :debounced ? apply_reasoning_effort(effort, conversation, provider: provider) : set_reasoning_effort(effort, conversation, provider: provider)
|
|
206
|
+
true
|
|
207
|
+
rescue StandardError => e
|
|
208
|
+
runtime_output("Reasoning error: #{e.message}")
|
|
209
|
+
false
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def set_reasoning_effort(effort, conversation = nil, provider: nil)
|
|
213
|
+
@pending_reasoning_config_mutex.synchronize { @pending_reasoning_config = nil }
|
|
214
|
+
persist_reasoning_config(effort, provider: provider)
|
|
215
|
+
apply_reasoning_effort(effort, conversation, provider: provider, queue_config: false)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def apply_reasoning_effort(effort, conversation = nil, provider: nil, queue_config: true)
|
|
219
|
+
queue_reasoning_config(effort, provider: provider, conversation: conversation) if queue_config
|
|
220
|
+
if queue_config
|
|
221
|
+
update_conversation_reasoning_effort(conversation, effort)
|
|
222
|
+
refresh_composer_status
|
|
223
|
+
else
|
|
224
|
+
refresh_conversation_runtime(conversation, reasoning_effort: effort)
|
|
225
|
+
refresh_composer_status
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def update_conversation_reasoning_effort(conversation, effort)
|
|
230
|
+
return unless conversation&.respond_to?(:update_runtime_context!)
|
|
231
|
+
|
|
232
|
+
conversation.update_runtime_context!(
|
|
233
|
+
provider: conversation.provider || current_model_provider,
|
|
234
|
+
model: conversation.model || current_model_id,
|
|
235
|
+
reasoning_effort: effort,
|
|
236
|
+
refresh: false
|
|
237
|
+
)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def pending_reasoning_effort(provider)
|
|
241
|
+
@pending_reasoning_config_mutex.synchronize do
|
|
242
|
+
pending = @pending_reasoning_config
|
|
243
|
+
return nil unless pending
|
|
244
|
+
return nil unless pending[:provider].to_s.downcase == provider.to_s.downcase
|
|
245
|
+
|
|
246
|
+
pending[:effort]
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def queue_reasoning_config(effort, provider: nil, conversation: nil)
|
|
251
|
+
pending = {
|
|
252
|
+
effort: effort,
|
|
253
|
+
provider: provider || current_model_provider,
|
|
254
|
+
conversation: conversation,
|
|
255
|
+
deadline: Process.clock_gettime(Process::CLOCK_MONOTONIC) + REASONING_CONFIG_DEBOUNCE_SECONDS
|
|
256
|
+
}
|
|
257
|
+
@pending_reasoning_config_mutex.synchronize { @pending_reasoning_config = pending }
|
|
258
|
+
schedule_reasoning_config_flush
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def schedule_reasoning_config_flush
|
|
262
|
+
return if @pending_reasoning_config_thread&.alive?
|
|
263
|
+
|
|
264
|
+
@pending_reasoning_config_thread = Thread.new do
|
|
265
|
+
loop do
|
|
266
|
+
sleep REASONING_CONFIG_DEBOUNCE_SECONDS
|
|
267
|
+
break if flush_pending_reasoning_config(force: false)
|
|
268
|
+
break unless @pending_reasoning_config_mutex.synchronize { @pending_reasoning_config }
|
|
269
|
+
end
|
|
270
|
+
rescue StandardError => e
|
|
271
|
+
runtime_output("Reasoning error: #{e.message}")
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def flush_pending_reasoning_config(force: true, conversation: nil)
|
|
276
|
+
pending = nil
|
|
277
|
+
@pending_reasoning_config_mutex.synchronize do
|
|
278
|
+
pending = @pending_reasoning_config
|
|
279
|
+
return false unless pending
|
|
280
|
+
|
|
281
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
282
|
+
return false if !force && now < pending[:deadline].to_f
|
|
283
|
+
|
|
284
|
+
@pending_reasoning_config = nil
|
|
285
|
+
end
|
|
286
|
+
persist_reasoning_config(pending[:effort], provider: pending[:provider])
|
|
287
|
+
conversation ||= pending[:conversation]
|
|
288
|
+
if conversation&.reasoning_effort.to_s == pending[:effort].to_s
|
|
289
|
+
refresh_conversation_runtime(conversation, reasoning_effort: pending[:effort])
|
|
290
|
+
conversation.persist_runtime_context! if conversation.respond_to?(:persist_runtime_context!)
|
|
291
|
+
end
|
|
292
|
+
true
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def persist_reasoning_config(effort, provider: nil)
|
|
296
|
+
ConfigFiles.update_config(ModelInfo.reasoning_config_key_for_provider(provider || current_model_provider) => effort)
|
|
297
|
+
reload_client_config
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def reasoning_choices(choices, conversation = current_footer_conversation)
|
|
301
|
+
current = (conversation.reasoning_effort || (@client.respond_to?(:current_reasoning_effort) ? @client.current_reasoning_effort : ModelInfo::DEFAULT_REASONING_EFFORT)).to_s
|
|
302
|
+
choices.map do |effort, label|
|
|
303
|
+
text = label.dup
|
|
304
|
+
text += " (current)" if current == effort
|
|
305
|
+
text
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def overlay_alignment_choices(settings)
|
|
310
|
+
ConfigFiles::OVERLAY_ALIGNMENTS.map do |alignment|
|
|
311
|
+
label = alignment.capitalize
|
|
312
|
+
label += " (current)" if settings["alignment"] == alignment
|
|
313
|
+
label
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def overlay_width_choices(settings)
|
|
318
|
+
ConfigFiles::OVERLAY_WIDTHS.map do |width|
|
|
319
|
+
label = width.capitalize
|
|
320
|
+
label += " (current)" if settings["width"] == width
|
|
321
|
+
label
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
end
|