openclacky 1.5.13 → 1.5.15
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 +96 -0
- data/lib/clacky/agent/chunk_index.rb +83 -0
- data/lib/clacky/agent/history_navigation.rb +239 -0
- data/lib/clacky/agent/session_serializer.rb +53 -98
- data/lib/clacky/agent.rb +477 -297
- data/lib/clacky/agent_config.rb +5 -3
- data/lib/clacky/billing/billing_store.rb +2 -2
- data/lib/clacky/billing/platform_billing.rb +7 -0
- data/lib/clacky/brand_config.rb +27 -19
- data/lib/clacky/cli.rb +92 -12
- data/lib/clacky/cli_guidance.rb +48 -0
- data/lib/clacky/client.rb +2 -2
- data/lib/clacky/default_extensions/ext-studio/agents/ext-developer/system_prompt.md +70 -134
- data/lib/clacky/default_extensions/ext-studio/api/handler.rb +4 -1
- data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +307 -98
- data/lib/clacky/default_extensions/ext-studio/skills/ext-develop/SKILL.md +179 -577
- data/lib/clacky/default_extensions/git/panels/git/view.js +34 -14
- data/lib/clacky/default_extensions/preview/ext.yml +20 -0
- data/lib/clacky/default_extensions/preview/panels/preview/view.js +475 -0
- data/lib/clacky/default_extensions/time_machine/panels/time_machine/view.js +1 -10
- data/lib/clacky/extension/api_extension.rb +18 -6
- data/lib/clacky/extension/verifier.rb +1 -1
- data/lib/clacky/json_ui_controller.rb +4 -2
- data/lib/clacky/media/openai_compat.rb +70 -30
- data/lib/clacky/message_format/bedrock.rb +6 -1
- data/lib/clacky/message_format/open_ai.rb +22 -5
- data/lib/clacky/message_format/open_ai_responses.rb +7 -3
- data/lib/clacky/plain_ui_controller.rb +1 -1
- data/lib/clacky/prompts/base.md +1 -1
- data/lib/clacky/providers.rb +44 -13
- data/lib/clacky/rich_ui/components/composer_guidance.rb +49 -0
- data/lib/clacky/rich_ui/rich_ui_controller.rb +24 -4
- data/lib/clacky/rich_ui/shell/rich_agent_shell.rb +13 -0
- data/lib/clacky/search_config.rb +3 -3
- data/lib/clacky/server/channel/channel_manager.rb +25 -7
- data/lib/clacky/server/channel/channel_ui_controller.rb +1 -1
- data/lib/clacky/server/dir_picker.rb +67 -4
- data/lib/clacky/server/git_panel.rb +10 -2
- data/lib/clacky/server/http_server.rb +391 -75
- data/lib/clacky/server/preview.rb +351 -0
- data/lib/clacky/server/session_registry.rb +4 -0
- data/lib/clacky/server/web_ui_controller.rb +10 -5
- data/lib/clacky/session_manager.rb +52 -1
- data/lib/clacky/skill.rb +48 -0
- data/lib/clacky/tools/browser.rb +176 -19
- data/lib/clacky/tools/web_search.rb +91 -8
- data/lib/clacky/ui2/components/command_suggestions.rb +3 -1
- data/lib/clacky/ui2/components/input_area.rb +37 -4
- data/lib/clacky/ui2/components/modal_component.rb +35 -6
- data/lib/clacky/ui2/screen_buffer.rb +1 -0
- data/lib/clacky/ui2/ui_controller.rb +72 -12
- data/lib/clacky/ui_interface.rb +1 -1
- data/lib/clacky/utils/file_processor.rb +19 -7
- data/lib/clacky/utils/mac_app_detector.rb +186 -0
- data/lib/clacky/utils/model_pricing.rb +118 -68
- data/lib/clacky/utils/windows_app_detector.rb +334 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +998 -168
- data/lib/clacky/web/app.js +19 -0
- data/lib/clacky/web/components/chat-navigator.js +489 -202
- data/lib/clacky/web/components/code-editor.js +191 -5
- data/lib/clacky/web/components/composer.js +3 -1
- data/lib/clacky/web/components/datepicker.js +19 -19
- data/lib/clacky/web/components/mentions.js +11 -14
- data/lib/clacky/web/components/model-picker.js +66 -23
- data/lib/clacky/web/components/quote-select.js +341 -0
- data/lib/clacky/web/core/aside.js +157 -8
- data/lib/clacky/web/core/ext.js +16 -0
- data/lib/clacky/web/features/billing/view.js +75 -21
- data/lib/clacky/web/features/extensions/store.js +4 -1
- data/lib/clacky/web/features/model-tester/store.js +37 -4
- data/lib/clacky/web/features/skills/store.js +18 -1
- data/lib/clacky/web/features/skills/view.js +128 -7
- data/lib/clacky/web/features/workspace/store.js +80 -7
- data/lib/clacky/web/features/workspace/view.js +474 -43
- data/lib/clacky/web/i18n.js +158 -23
- data/lib/clacky/web/index.html +57 -29
- data/lib/clacky/web/sessions.js +517 -127
- data/lib/clacky/web/settings.js +155 -6
- data/lib/clacky/web/utils.js +34 -0
- data/lib/clacky/web/vendor/codemirror/codemirror.min.js +24 -19
- data/lib/clacky/web/vendor/codemirror/entry.js +130 -0
- data/lib/clacky/web/vendor/codemirror/package.json +29 -0
- data/lib/clacky/web/ws-dispatcher.js +161 -4
- data/lib/clacky.rb +2 -0
- metadata +13 -1
|
@@ -17,7 +17,7 @@ module Clacky
|
|
|
17
17
|
#
|
|
18
18
|
# class MyDashboardExt < Clacky::ApiExtension
|
|
19
19
|
# get "/summary" do
|
|
20
|
-
# json(sessions: session_manager.
|
|
20
|
+
# json(sessions: session_manager.all_sessions.size)
|
|
21
21
|
# end
|
|
22
22
|
# end
|
|
23
23
|
#
|
|
@@ -272,9 +272,10 @@ module Clacky
|
|
|
272
272
|
# controls the user-facing bubble shown in place of the raw prompt.
|
|
273
273
|
# When `project_id` is given, the project's working_dir is inherited
|
|
274
274
|
# (unless an explicit working_dir overrides it) and the session is
|
|
275
|
-
# associated with the project.
|
|
275
|
+
# associated with the project. `model_id` selects a configured model by
|
|
276
|
+
# its stable id; omitting it keeps the current default model.
|
|
276
277
|
def create_session(name: nil, prompt: nil, working_dir: nil, profile: "general",
|
|
277
|
-
source: :manual, display_message: nil, project_id: nil)
|
|
278
|
+
source: :manual, display_message: nil, project_id: nil, model_id: nil)
|
|
278
279
|
error!("server not ready", status: 503) unless @http_server
|
|
279
280
|
|
|
280
281
|
src = source.to_s
|
|
@@ -290,6 +291,14 @@ module Clacky
|
|
|
290
291
|
project = project_id ? project_manager&.find(project_id) : nil
|
|
291
292
|
error!("Project not found", status: 404) if project_id && project.nil?
|
|
292
293
|
|
|
294
|
+
if model_id
|
|
295
|
+
model_id = model_id.to_s.strip
|
|
296
|
+
model_id = nil if model_id.empty?
|
|
297
|
+
end
|
|
298
|
+
if model_id && !agent_config.models.any? { |model| model["id"] == model_id }
|
|
299
|
+
error!("Model not found in configuration", status: 400)
|
|
300
|
+
end
|
|
301
|
+
|
|
293
302
|
working_dir = File.expand_path(project[:working_dir]) if working_dir.nil? && project && project[:working_dir].to_s.strip != ""
|
|
294
303
|
|
|
295
304
|
session_id = @http_server.send(
|
|
@@ -297,7 +306,8 @@ module Clacky
|
|
|
297
306
|
name: name,
|
|
298
307
|
working_dir: working_dir,
|
|
299
308
|
profile: profile,
|
|
300
|
-
source: source
|
|
309
|
+
source: source,
|
|
310
|
+
model_id: model_id
|
|
301
311
|
)
|
|
302
312
|
|
|
303
313
|
if project_id
|
|
@@ -309,9 +319,11 @@ module Clacky
|
|
|
309
319
|
end
|
|
310
320
|
end
|
|
311
321
|
|
|
312
|
-
|
|
322
|
+
# Announce creation only after identity/placement metadata is persisted,
|
|
323
|
+
# and before submit_task can emit ordinary running-state updates.
|
|
324
|
+
@http_server.send(:broadcast_session_update, session_id, created: true)
|
|
313
325
|
|
|
314
|
-
|
|
326
|
+
submit_task(session_id, prompt, display_message: display_message) if prompt && !prompt.strip.empty?
|
|
315
327
|
|
|
316
328
|
session_id
|
|
317
329
|
end
|
|
@@ -22,7 +22,7 @@ module Clacky
|
|
|
22
22
|
PANEL_KEYS = %w[id title title_zh description description_zh view order attach entry_points].freeze
|
|
23
23
|
API_KEYS = %w[id handler].freeze
|
|
24
24
|
SKILL_KEYS = %w[id dir protected].freeze
|
|
25
|
-
AGENT_KEYS = %w[id title title_zh description description_zh order prompt panels skills avatar hidden tools].freeze
|
|
25
|
+
AGENT_KEYS = %w[id title title_zh description description_zh order prompt panels skills disabled_skills avatar hidden tools].freeze
|
|
26
26
|
CHANNEL_KEYS = %w[id platform adapter].freeze
|
|
27
27
|
PATCH_KEYS = %w[target file fingerprint on_mismatch].freeze
|
|
28
28
|
HOOK_KEYS = %w[event file].freeze
|
|
@@ -41,8 +41,10 @@ module Clacky
|
|
|
41
41
|
emit("tool_call", name: name, args: args_data)
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
-
def show_tool_result(result)
|
|
45
|
-
|
|
44
|
+
def show_tool_result(result, ui: nil)
|
|
45
|
+
data = { result: result }
|
|
46
|
+
data[:ui] = ui if ui
|
|
47
|
+
emit("tool_result", **data)
|
|
46
48
|
end
|
|
47
49
|
|
|
48
50
|
def show_tool_error(error)
|
|
@@ -308,38 +308,69 @@ module Clacky
|
|
|
308
308
|
end
|
|
309
309
|
|
|
310
310
|
ext = mime_type.split(";").first.split("/").last.then { |e| e == "mpeg" ? "mp3" : e }
|
|
311
|
-
filename = "chunk.#{ext}"
|
|
312
|
-
audio_data = Base64.decode64(audio_base64)
|
|
313
|
-
boundary = "----FormBoundary#{SecureRandom.hex(8)}"
|
|
314
|
-
# A multipart body is a byte stream: build it in binary so UTF-8 text
|
|
315
|
-
# parts (e.g. a non-ASCII vocabulary prompt) don't clash with the
|
|
316
|
-
# ASCII-8BIT audio bytes.
|
|
317
|
-
body = "".b
|
|
318
|
-
body << "--#{boundary}\r\n".b
|
|
319
|
-
body << "Content-Disposition: form-data; name=\"file\"; filename=\"#{filename}\"\r\n".b
|
|
320
|
-
body << "Content-Type: #{mime_type.split(';').first}\r\n\r\n".b
|
|
321
|
-
body << audio_data.b
|
|
322
|
-
body << "\r\n--#{boundary}\r\n".b
|
|
323
|
-
body << "Content-Disposition: form-data; name=\"model\"\r\n\r\n".b
|
|
324
|
-
body << @model.to_s.b
|
|
325
|
-
unless prompt.to_s.strip.empty?
|
|
326
|
-
body << "\r\n--#{boundary}\r\n".b
|
|
327
|
-
body << "Content-Disposition: form-data; name=\"prompt\"\r\n\r\n".b
|
|
328
|
-
body << prompt.to_s.strip.b
|
|
329
|
-
end
|
|
330
|
-
body << "\r\n--#{boundary}--\r\n".b
|
|
331
311
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
312
|
+
# OpenRouter caps multipart uploads at 25 MB and redirects larger files
|
|
313
|
+
# to base64 JSON via input_audio. Plain OpenAI-compatible providers keep
|
|
314
|
+
# multipart (incl. the openclacky gateway, which only parses multipart
|
|
315
|
+
# on its /audio/transcriptions today — enable the JSON flavor there once
|
|
316
|
+
# the server supports it).
|
|
317
|
+
if provider_id == Clacky::Providers::OPENROUTER_ID
|
|
318
|
+
ext = "m4a" if ext == "mp4" # OpenRouter wants the audio format, not the container
|
|
319
|
+
payload = {
|
|
320
|
+
model: @model,
|
|
321
|
+
input_audio: {
|
|
322
|
+
data: audio_base64, # raw base64 bytes, NOT a data URI
|
|
323
|
+
format: ext
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
payload[:prompt] = prompt.to_s.strip unless prompt.to_s.strip.empty?
|
|
327
|
+
|
|
328
|
+
begin
|
|
329
|
+
response = stt_json_connection.post("audio/transcriptions") do |req|
|
|
330
|
+
req.headers["Content-Type"] = "application/json"
|
|
331
|
+
req.headers["Authorization"] = "Bearer #{@api_key}"
|
|
332
|
+
req.body = JSON.generate(payload)
|
|
333
|
+
end
|
|
334
|
+
rescue Faraday::Error => e
|
|
335
|
+
return transcription_error_response(
|
|
336
|
+
error: "HTTP request failed: #{e.message}",
|
|
337
|
+
error_type: "network_error", provider: provider_id
|
|
338
|
+
)
|
|
339
|
+
end
|
|
340
|
+
else
|
|
341
|
+
filename = "chunk.#{ext}"
|
|
342
|
+
audio_data = Base64.decode64(audio_base64)
|
|
343
|
+
boundary = "----FormBoundary#{SecureRandom.hex(8)}"
|
|
344
|
+
# A multipart body is a byte stream: build it in binary so UTF-8 text
|
|
345
|
+
# parts (e.g. a non-ASCII vocabulary prompt) don't clash with the
|
|
346
|
+
# ASCII-8BIT audio bytes.
|
|
347
|
+
body = "".b
|
|
348
|
+
body << "--#{boundary}\r\n".b
|
|
349
|
+
body << "Content-Disposition: form-data; name=\"file\"; filename=\"#{filename}\"\r\n".b
|
|
350
|
+
body << "Content-Type: #{mime_type.split(';').first}\r\n\r\n".b
|
|
351
|
+
body << audio_data.b
|
|
352
|
+
body << "\r\n--#{boundary}\r\n".b
|
|
353
|
+
body << "Content-Disposition: form-data; name=\"model\"\r\n\r\n".b
|
|
354
|
+
body << @model.to_s.b
|
|
355
|
+
unless prompt.to_s.strip.empty?
|
|
356
|
+
body << "\r\n--#{boundary}\r\n".b
|
|
357
|
+
body << "Content-Disposition: form-data; name=\"prompt\"\r\n\r\n".b
|
|
358
|
+
body << prompt.to_s.strip.b
|
|
359
|
+
end
|
|
360
|
+
body << "\r\n--#{boundary}--\r\n".b
|
|
361
|
+
|
|
362
|
+
begin
|
|
363
|
+
response = stt_connection.post("audio/transcriptions") do |req|
|
|
364
|
+
req.headers["Content-Type"] = "multipart/form-data; boundary=#{boundary}"
|
|
365
|
+
req.headers["Authorization"] = "Bearer #{@api_key}"
|
|
366
|
+
req.body = body
|
|
367
|
+
end
|
|
368
|
+
rescue Faraday::Error => e
|
|
369
|
+
return transcription_error_response(
|
|
370
|
+
error: "HTTP request failed: #{e.message}",
|
|
371
|
+
error_type: "network_error", provider: provider_id
|
|
372
|
+
)
|
|
337
373
|
end
|
|
338
|
-
rescue Faraday::Error => e
|
|
339
|
-
return transcription_error_response(
|
|
340
|
-
error: "HTTP request failed: #{e.message}",
|
|
341
|
-
error_type: "network_error", provider: provider_id
|
|
342
|
-
)
|
|
343
374
|
end
|
|
344
375
|
|
|
345
376
|
unless response.success?
|
|
@@ -479,6 +510,15 @@ module Clacky
|
|
|
479
510
|
end
|
|
480
511
|
end
|
|
481
512
|
|
|
513
|
+
# STT JSON body carries a large base64 payload; upstream may need longer
|
|
514
|
+
# to decode/transcribe it than the 30s multipart path.
|
|
515
|
+
private def stt_json_connection
|
|
516
|
+
Faraday.new(url: normalized_base_url) do |f|
|
|
517
|
+
f.options.timeout = 120
|
|
518
|
+
f.options.open_timeout = 10
|
|
519
|
+
end
|
|
520
|
+
end
|
|
521
|
+
|
|
482
522
|
private def vu_connection
|
|
483
523
|
Faraday.new(url: normalized_base_url) do |f|
|
|
484
524
|
f.options.timeout = 60
|
|
@@ -26,6 +26,8 @@ module Clacky
|
|
|
26
26
|
# the same Clacky proxy on a different endpoint. The *model prefix* is
|
|
27
27
|
# the source of truth for which upstream format the proxy expects:
|
|
28
28
|
#
|
|
29
|
+
# abs-gpt-* → OpenAI-compatible (POST /chat/completions) — GPT models
|
|
30
|
+
# on Bedrock use the OpenAI endpoint, not Converse
|
|
29
31
|
# abs-* → Bedrock Converse (POST /model/{id}/converse)
|
|
30
32
|
# dsk-* → OpenAI-compatible (POST /chat/completions)
|
|
31
33
|
# or-* → OpenAI-compatible (POST /chat/completions)
|
|
@@ -38,7 +40,10 @@ module Clacky
|
|
|
38
40
|
# will route correctly without touching this file.
|
|
39
41
|
def self.bedrock_api_key?(api_key, model)
|
|
40
42
|
return true if api_key.to_s.start_with?("ABSK")
|
|
41
|
-
model.to_s
|
|
43
|
+
model_str = model.to_s
|
|
44
|
+
# abs-gpt-* is served through Bedrock's OpenAI-compatible endpoint.
|
|
45
|
+
return false if model_str.start_with?("abs-gpt-")
|
|
46
|
+
model_str.start_with?("abs-")
|
|
42
47
|
end
|
|
43
48
|
|
|
44
49
|
module_function
|
|
@@ -217,9 +217,13 @@ module Clacky
|
|
|
217
217
|
# unchanged, preserving the provider default for all models.
|
|
218
218
|
#
|
|
219
219
|
# Single source of truth for reasoning param mapping — shared with
|
|
220
|
-
# MessageFormat::OpenAIResponses
|
|
221
|
-
#
|
|
222
|
-
|
|
220
|
+
# MessageFormat::OpenAIResponses.
|
|
221
|
+
#
|
|
222
|
+
# @param responses_api [Boolean] emit the generic effort as the Responses
|
|
223
|
+
# API's nested `reasoning: { effort: ... }`. The top-level
|
|
224
|
+
# `reasoning_effort` field is Chat Completions only — the Responses API
|
|
225
|
+
# rejects it with "Unknown parameter: 'reasoning_effort'".
|
|
226
|
+
def self.apply_reasoning_params(body, model, reasoning_effort, responses_api: false)
|
|
223
227
|
effort_str = reasoning_effort.to_s
|
|
224
228
|
|
|
225
229
|
if model.to_s.match?(/\Aglm-5[-.]3/i)
|
|
@@ -306,9 +310,22 @@ module Clacky
|
|
|
306
310
|
when "xhigh" then "max"
|
|
307
311
|
else effort_str
|
|
308
312
|
end
|
|
309
|
-
body
|
|
313
|
+
assign_effort(body, effort, responses_api)
|
|
310
314
|
elsif reasoning_effort && !effort_str.empty?
|
|
311
|
-
body
|
|
315
|
+
assign_effort(body, effort_str, responses_api)
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
# Write the resolved effort into the field shape the target API expects.
|
|
320
|
+
# Chat Completions takes the top-level reasoning_effort; the Responses API
|
|
321
|
+
# takes reasoning.effort and errors out on the flat field.
|
|
322
|
+
private_class_method def self.assign_effort(body, effort, responses_api)
|
|
323
|
+
return if effort.nil? || effort.empty?
|
|
324
|
+
|
|
325
|
+
if responses_api
|
|
326
|
+
body[:reasoning] = { effort: effort }
|
|
327
|
+
else
|
|
328
|
+
body[:reasoning_effort] = effort
|
|
312
329
|
end
|
|
313
330
|
end
|
|
314
331
|
|
|
@@ -53,7 +53,7 @@ module Clacky
|
|
|
53
53
|
body[:tools] = convert_tools_to_responses_format(tools)
|
|
54
54
|
end
|
|
55
55
|
|
|
56
|
-
OpenAI.apply_reasoning_params(body, model, reasoning_effort)
|
|
56
|
+
OpenAI.apply_reasoning_params(body, model, reasoning_effort, responses_api: true)
|
|
57
57
|
|
|
58
58
|
body
|
|
59
59
|
end
|
|
@@ -156,8 +156,12 @@ module Clacky
|
|
|
156
156
|
{ type: "input_text", text: text }
|
|
157
157
|
when "image_url"
|
|
158
158
|
if vision_supported
|
|
159
|
-
# Responses API
|
|
160
|
-
{
|
|
159
|
+
# Responses API takes the URL as a plain string — the Chat
|
|
160
|
+
# Completions { url: ... } wrapper fails schema validation with
|
|
161
|
+
# "Invalid 'input': value did not match any expected variant".
|
|
162
|
+
url = block[:image_url]
|
|
163
|
+
url = url[:url] || url["url"] if url.is_a?(Hash)
|
|
164
|
+
{ type: "input_image", image_url: url }
|
|
161
165
|
else
|
|
162
166
|
{ type: "input_text", text: "[Image content removed - current model does not support vision input]" }
|
|
163
167
|
end
|
data/lib/clacky/prompts/base.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
- Ask clarifying questions if requirements are unclear.
|
|
4
4
|
- Break down complex tasks into manageable steps.
|
|
5
5
|
- **USE TOOLS to create/modify files** — don't just return content.
|
|
6
|
-
- When the user asks to send/download a file or you generate one for them, append `[filename](file
|
|
6
|
+
- When the user asks to send/download a file or you generate one for them, append `[filename](file:///absolute/path/to/file)` at the end of your reply. Use the exact absolute path the write tool returned — don't rewrite or abbreviate it with `~` (keep non-ASCII characters and spaces literal); never URL-encode it.
|
|
7
7
|
|
|
8
8
|
## Tool Usage Rules
|
|
9
9
|
|
data/lib/clacky/providers.rb
CHANGED
|
@@ -29,8 +29,12 @@ module Clacky
|
|
|
29
29
|
"name" => "OpenClacky",
|
|
30
30
|
"base_url" => "https://api.openclacky.com",
|
|
31
31
|
"api" => "bedrock",
|
|
32
|
-
"default_model" => "
|
|
32
|
+
"default_model" => "or-gemini-3-8-flash",
|
|
33
33
|
"models" => [
|
|
34
|
+
"abs-gpt-6-astra",
|
|
35
|
+
"abs-gpt-5.6-sol",
|
|
36
|
+
"abs-gpt-5.6-terra",
|
|
37
|
+
"abs-gpt-5.6-luna",
|
|
34
38
|
"abs-claude-fable-5-1",
|
|
35
39
|
"abs-claude-fable-5",
|
|
36
40
|
"abs-claude-opus-5",
|
|
@@ -41,9 +45,8 @@ module Clacky
|
|
|
41
45
|
"abs-claude-sonnet-4-6",
|
|
42
46
|
"abs-claude-sonnet-4-5",
|
|
43
47
|
"abs-claude-haiku-4-5",
|
|
48
|
+
"dsk-deepseek-flash",
|
|
44
49
|
"dsk-deepseek-v4-pro",
|
|
45
|
-
"dsk-deepseek-v4-flash",
|
|
46
|
-
"dsk-deepseek-v4-flash-vision-exp",
|
|
47
50
|
"or-gemini-3-1-pro",
|
|
48
51
|
"or-gemini-3-8-flash",
|
|
49
52
|
"or-gemini-3-7-flash",
|
|
@@ -134,23 +137,28 @@ module Clacky
|
|
|
134
137
|
# Default OCR sidecar — used when the primary model is text-only.
|
|
135
138
|
# Candidates are derived from the provider's vision-capable models;
|
|
136
139
|
# this just picks the cheap+fast default to surface in "auto" mode.
|
|
137
|
-
"default_ocr_model" => "
|
|
140
|
+
"default_ocr_model" => "or-gemini-3-8-flash",
|
|
138
141
|
# Provider-level default: the Claude family served here is vision-capable.
|
|
139
142
|
"capabilities" => { "vision" => true }.freeze,
|
|
140
143
|
# Model-level overrides: DeepSeek models routed through this provider
|
|
141
|
-
# are text-only
|
|
142
|
-
#
|
|
143
|
-
#
|
|
144
|
+
# are text-only except V4.1 Flash, which is natively multimodal;
|
|
145
|
+
# images uploaded for text-only models must be downgraded to disk
|
|
146
|
+
# refs. Gemini 3.1 Pro keeps the provider-default vision=true
|
|
144
147
|
# (it accepts image/audio/video input natively via OpenRouter).
|
|
145
148
|
"model_capabilities" => {
|
|
146
|
-
"dsk-deepseek-
|
|
147
|
-
"dsk-deepseek-v4-
|
|
148
|
-
|
|
149
|
+
"dsk-deepseek-flash" => { "vision" => true }.freeze,
|
|
150
|
+
"dsk-deepseek-v4-pro" => { "vision" => false }.freeze
|
|
151
|
+
}.freeze,
|
|
152
|
+
# Bedrock GPT models (abs-gpt-*) are served through the OpenAI
|
|
153
|
+
# Responses API — their Chat Completions endpoint rejects function
|
|
154
|
+
# tools for reasoning models (GPT-6 Astra).
|
|
155
|
+
"model_api_overrides" => {
|
|
156
|
+
/\Aabs-gpt-/ => "openai-responses"
|
|
149
157
|
}.freeze,
|
|
150
158
|
# Per-primary lite pairing: keys are "strong" primary models, values
|
|
151
159
|
# are the lite sidekick to auto-inject when that primary is the
|
|
152
160
|
# default. Lite is consumed by some subagents for cheap/fast work;
|
|
153
|
-
# weak models (haiku /
|
|
161
|
+
# weak models (haiku / flash / 3-5-flash) ARE the lite tier
|
|
154
162
|
# themselves, so they're intentionally not listed here as keys —
|
|
155
163
|
# no injection happens when the default model is already lite-class.
|
|
156
164
|
"lite_models" => {
|
|
@@ -163,7 +171,9 @@ module Clacky
|
|
|
163
171
|
"abs-claude-sonnet-5" => "abs-claude-haiku-4-5",
|
|
164
172
|
"abs-claude-sonnet-4-6" => "abs-claude-haiku-4-5",
|
|
165
173
|
"abs-claude-sonnet-4-5" => "abs-claude-haiku-4-5",
|
|
166
|
-
"
|
|
174
|
+
"abs-gpt-5.6-sol" => "abs-gpt-5.6-luna",
|
|
175
|
+
"abs-gpt-5.6-terra" => "abs-gpt-5.6-luna",
|
|
176
|
+
"dsk-deepseek-v4-pro" => "dsk-deepseek-flash",
|
|
167
177
|
"or-gemini-3-1-pro" => "or-gemini-3-6-flash"
|
|
168
178
|
},
|
|
169
179
|
# Fallback chain: if a model is unavailable, try the next one in order.
|
|
@@ -266,14 +276,17 @@ module Clacky
|
|
|
266
276
|
# deprecated on 2026-07-24; they map to deepseek-v4-flash's non-thinking
|
|
267
277
|
# and thinking modes respectively. Prefer deepseek-v4-flash / deepseek-v4-pro.
|
|
268
278
|
"models" => [
|
|
279
|
+
"deepseek-flash",
|
|
269
280
|
"deepseek-v4-pro",
|
|
270
281
|
"deepseek-v4-flash",
|
|
271
282
|
"deepseek-v4-flash-vision-exp",
|
|
272
283
|
],
|
|
273
284
|
# DeepSeek V4 API is text-only across all models, except the
|
|
274
|
-
# flash-vision-exp variant which accepts image input.
|
|
285
|
+
# flash-vision-exp variant which accepts image input. V4.1 Flash
|
|
286
|
+
# (deepseek-flash) is natively multimodal.
|
|
275
287
|
"capabilities" => { "vision" => false }.freeze,
|
|
276
288
|
"model_capabilities" => {
|
|
289
|
+
"deepseek-flash" => { "vision" => true }.freeze,
|
|
277
290
|
"deepseek-v4-flash-vision-exp" => { "vision" => true }.freeze
|
|
278
291
|
}.freeze,
|
|
279
292
|
"website_url" => "https://platform.deepseek.com/api_keys"
|
|
@@ -420,6 +433,7 @@ module Clacky
|
|
|
420
433
|
"api" => "openai-completions",
|
|
421
434
|
"default_model" => "gpt-5.5",
|
|
422
435
|
"models" => [
|
|
436
|
+
"gpt-6-astra",
|
|
423
437
|
"gpt-5.6-sol",
|
|
424
438
|
"gpt-5.6-terra",
|
|
425
439
|
"gpt-5.6-luna",
|
|
@@ -703,6 +717,23 @@ module Clacky
|
|
|
703
717
|
|
|
704
718
|
}.freeze
|
|
705
719
|
|
|
720
|
+
# Canonical provider identifiers. Mirror the PRESETS keys above; keep both
|
|
721
|
+
# in sync when adding or renaming a provider.
|
|
722
|
+
OPENCLACKY_ID = "openclacky"
|
|
723
|
+
OPENROUTER_ID = "openrouter"
|
|
724
|
+
DEEPSEEKV4_ID = "deepseekv4"
|
|
725
|
+
GLM_ID = "glm"
|
|
726
|
+
KIMI_ID = "kimi"
|
|
727
|
+
KIMI_CODING_ID = "kimi-coding"
|
|
728
|
+
MINIMAX_ID = "minimax"
|
|
729
|
+
ANTHROPIC_ID = "anthropic"
|
|
730
|
+
OPENAI_ID = "openai"
|
|
731
|
+
QWEN_ID = "qwen"
|
|
732
|
+
MIMO_ID = "mimo"
|
|
733
|
+
VOLCENGINE_ARK_ID = "volcengine-ark"
|
|
734
|
+
OLLAMA_ID = "ollama"
|
|
735
|
+
ORCAROUTER_ID = "orcarouter"
|
|
736
|
+
|
|
706
737
|
MEDIA_KINDS = %w[image video audio stt video_understanding].freeze
|
|
707
738
|
|
|
708
739
|
# Per-model maximum output token limits.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "unicode/display_width"
|
|
4
|
+
|
|
5
|
+
module Clacky
|
|
6
|
+
module RichUI
|
|
7
|
+
# Extend the composer's attachment area rather than adding a layout sibling.
|
|
8
|
+
# Keep pending entries out of @attachments so submitting/deleting a draft
|
|
9
|
+
# cannot accidentally upload or consume queued messages.
|
|
10
|
+
module ComposerGuidance
|
|
11
|
+
attr_accessor :guidance_lines_provider
|
|
12
|
+
|
|
13
|
+
def desired_height
|
|
14
|
+
super + pending_guidance_lines.size
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def native_cursor_position
|
|
18
|
+
return nil unless focused?
|
|
19
|
+
|
|
20
|
+
editor_row, editor_col = @editor.cursor_visual_position(width: [inner_width - 2, 1].max)
|
|
21
|
+
attachments = render_attachments
|
|
22
|
+
raw_row = attachments.size + editor_row
|
|
23
|
+
lines = attachments + render_input_lines
|
|
24
|
+
lines.concat(render_menu_lines) if menu_open?
|
|
25
|
+
height = [@height.to_i, 1].max
|
|
26
|
+
visible_row = raw_row - [lines.size - height, 0].max
|
|
27
|
+
return nil if visible_row.negative? || visible_row >= height
|
|
28
|
+
|
|
29
|
+
[visible_row, 2 + editor_col]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private def pending_guidance_lines
|
|
33
|
+
@guidance_lines_provider&.call || []
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private def render_attachments
|
|
37
|
+
super + pending_guidance_lines.map do |line|
|
|
38
|
+
used = 0
|
|
39
|
+
clipped = line.each_char.take_while do |char|
|
|
40
|
+
used += Unicode::DisplayWidth.of(char)
|
|
41
|
+
used <= [inner_width - 1, 0].max
|
|
42
|
+
end.join
|
|
43
|
+
clipped += "…" if clipped.length < line.length && inner_width.positive?
|
|
44
|
+
clipped
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -6,6 +6,7 @@ require "base64"
|
|
|
6
6
|
require "set"
|
|
7
7
|
require "ruby_rich"
|
|
8
8
|
require_relative "../ui_interface"
|
|
9
|
+
require_relative "../cli_guidance"
|
|
9
10
|
require_relative "../providers"
|
|
10
11
|
require_relative "../ui2/components/welcome_banner"
|
|
11
12
|
require_relative "shell/rich_agent_shell"
|
|
@@ -22,6 +23,7 @@ require_relative "components/dialogs/approval_dialog"
|
|
|
22
23
|
module Clacky
|
|
23
24
|
class RichUIController
|
|
24
25
|
include Clacky::UIInterface
|
|
26
|
+
include Clacky::CliGuidance
|
|
25
27
|
include Clacky::RichUI::ViewRenderer
|
|
26
28
|
|
|
27
29
|
STREAMING_MARKDOWN_THRESHOLD = 240
|
|
@@ -151,6 +153,7 @@ module Clacky
|
|
|
151
153
|
def set_skill_loader(skill_loader, agent_profile = nil)
|
|
152
154
|
return unless skill_loader
|
|
153
155
|
|
|
156
|
+
@shell.composer.register_command(name: "/input-mode", description: "Input behavior: steer or interrupt")
|
|
154
157
|
skills = skill_loader.user_invocable_skills(agent_profile)
|
|
155
158
|
|
|
156
159
|
skills.each do |skill|
|
|
@@ -166,6 +169,8 @@ module Clacky
|
|
|
166
169
|
|
|
167
170
|
def set_agent(_agent, _agent_profile = nil); end
|
|
168
171
|
|
|
172
|
+
attr_accessor :queue_input_while_running
|
|
173
|
+
|
|
169
174
|
def on_input(&block)
|
|
170
175
|
@input_callback = block
|
|
171
176
|
end
|
|
@@ -245,7 +250,7 @@ module Clacky
|
|
|
245
250
|
end
|
|
246
251
|
end
|
|
247
252
|
|
|
248
|
-
def show_tool_result(result)
|
|
253
|
+
def show_tool_result(result, ui: nil)
|
|
249
254
|
if (id = @tracker.pop_tool_id)
|
|
250
255
|
@shell.finish_tool_call(id, status: :done, output: format_tool_output(result.to_s, :done))
|
|
251
256
|
update_tool_activity(id, :done)
|
|
@@ -457,6 +462,20 @@ module Clacky
|
|
|
457
462
|
{ model: selected, persist: persist_choice }
|
|
458
463
|
end
|
|
459
464
|
|
|
465
|
+
def dispatch_guidance_action(action, id)
|
|
466
|
+
run_callback_async { @guidance_action&.call(action, id) }
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
def refresh_guidance
|
|
470
|
+
@shell.live&.refresh
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
def show_user_message(content, created_at: nil, files: [], source: :web, steering: false)
|
|
474
|
+
return unless steering
|
|
475
|
+
@shell.add_user_message(content)
|
|
476
|
+
add_file_summary(files) unless files.empty?
|
|
477
|
+
end
|
|
478
|
+
|
|
460
479
|
def clear_input
|
|
461
480
|
@shell.composer.editor.clear
|
|
462
481
|
end
|
|
@@ -470,6 +489,7 @@ module Clacky
|
|
|
470
489
|
Commands:
|
|
471
490
|
/clear - Clear output and restart session
|
|
472
491
|
/exit - Exit application
|
|
492
|
+
/input-mode [steer|interrupt] - View or change input behavior
|
|
473
493
|
|
|
474
494
|
Input:
|
|
475
495
|
Shift+Enter - New line
|
|
@@ -496,7 +516,7 @@ module Clacky
|
|
|
496
516
|
when :add
|
|
497
517
|
new_model = show_model_edit_form(nil, test_callback: test_callback)
|
|
498
518
|
if new_model
|
|
499
|
-
anthropic_format = new_model[:provider] ==
|
|
519
|
+
anthropic_format = new_model[:provider] == Clacky::Providers::ANTHROPIC_ID
|
|
500
520
|
current_config.add_model(
|
|
501
521
|
model: new_model[:model],
|
|
502
522
|
api_key: new_model[:api_key],
|
|
@@ -628,10 +648,10 @@ module Clacky
|
|
|
628
648
|
|
|
629
649
|
def wire_shell_callbacks
|
|
630
650
|
@shell.on_submit do |text, attachments|
|
|
631
|
-
reset_task_sidebar_tracking
|
|
651
|
+
reset_task_sidebar_tracking unless guidance_deferred?(text)
|
|
632
652
|
@ctrl_c_warning = nil
|
|
633
653
|
files = Array(attachments).map { |attachment| attachment.respond_to?(:to_h) ? attachment.to_h : attachment }
|
|
634
|
-
@shell.add_user_message(text)
|
|
654
|
+
@shell.add_user_message(text) unless guidance_deferred?(text)
|
|
635
655
|
run_callback_async { @input_callback&.call(text, files, display: text) }
|
|
636
656
|
end
|
|
637
657
|
|
|
@@ -4,6 +4,7 @@ require "ruby_rich"
|
|
|
4
4
|
require_relative "../components/sidebar"
|
|
5
5
|
require_relative "../components/thinking_live_view"
|
|
6
6
|
require_relative "../components/status_view"
|
|
7
|
+
require_relative "../components/composer_guidance"
|
|
7
8
|
|
|
8
9
|
module Clacky
|
|
9
10
|
class RichAgentShell < RubyRich::AgentShell
|
|
@@ -80,6 +81,8 @@ module Clacky
|
|
|
80
81
|
|
|
81
82
|
def attach_agent_controls
|
|
82
83
|
@composer.instance_variable_set(:@on_interrupt, nil)
|
|
84
|
+
@composer.singleton_class.prepend(RichUI::ComposerGuidance)
|
|
85
|
+
@composer.guidance_lines_provider = -> { @clacky_controller&.guidance_lines || [] }
|
|
83
86
|
# Register /model command
|
|
84
87
|
shell_ref = self
|
|
85
88
|
@composer.register_command(name: "/model", description: "Switch LLM model",
|
|
@@ -95,6 +98,16 @@ module Clacky
|
|
|
95
98
|
handled ? nil : native_escape.call
|
|
96
99
|
end
|
|
97
100
|
# Clear Ctrl+C warning as soon as the user starts typing
|
|
101
|
+
native_ctrl_d = @composer.method(:ctrl_d)
|
|
102
|
+
@composer.define_singleton_method(:ctrl_d) do |live|
|
|
103
|
+
handled = shell.clacky_controller&.request_guidance_action(:remove)
|
|
104
|
+
native_ctrl_d.call(live) unless handled
|
|
105
|
+
end
|
|
106
|
+
@layout.key(:ctrl_g, 250) do |_event, _live|
|
|
107
|
+
@clacky_controller&.request_guidance_action(:send_now) if @composer.focused?
|
|
108
|
+
false
|
|
109
|
+
end
|
|
110
|
+
|
|
98
111
|
native_insert = @composer.method(:insert_text)
|
|
99
112
|
@composer.define_singleton_method(:insert_text) do |text|
|
|
100
113
|
shell.callbacks[:clear_ctrlc]&.call
|
data/lib/clacky/search_config.rb
CHANGED
|
@@ -6,10 +6,10 @@ require "fileutils"
|
|
|
6
6
|
module Clacky
|
|
7
7
|
# Search provider selection, backed by ~/.clacky/search.yml.
|
|
8
8
|
#
|
|
9
|
-
# The built-in DuckDuckGo/Bing
|
|
10
|
-
# default. Pointing `provider` at a script in ~/.clacky/searchers/ routes
|
|
9
|
+
# The built-in Parallel/DuckDuckGo/Bing providers need no configuration and
|
|
10
|
+
# stay the default. Pointing `provider` at a script in ~/.clacky/searchers/ routes
|
|
11
11
|
# web_search through an API-backed engine instead, which is what users who
|
|
12
|
-
# want
|
|
12
|
+
# want a provider-specific API or policy controls reach for.
|
|
13
13
|
#
|
|
14
14
|
# search.yml schema:
|
|
15
15
|
# provider: tavily — basename of a script in ~/.clacky/searchers/;
|