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
data/lib/clacky/agent_config.rb
CHANGED
|
@@ -158,7 +158,7 @@ module Clacky
|
|
|
158
158
|
DEFAULT_COMPRESSION_THRESHOLD = 150_000
|
|
159
159
|
DEFAULT_MESSAGE_COUNT_THRESHOLD = 200
|
|
160
160
|
|
|
161
|
-
attr_accessor :permission_mode, :max_tokens, :verbose,
|
|
161
|
+
attr_accessor :input_behavior, :permission_mode, :max_tokens, :verbose,
|
|
162
162
|
:enable_compression, :enable_idle_compression, :enable_prompt_caching,
|
|
163
163
|
:compression_threshold, :message_count_threshold,
|
|
164
164
|
:models, :current_model_index, :current_model_id,
|
|
@@ -170,6 +170,7 @@ module Clacky
|
|
|
170
170
|
|
|
171
171
|
def initialize(options = {})
|
|
172
172
|
@permission_mode = validate_permission_mode(options[:permission_mode])
|
|
173
|
+
@input_behavior = %w[queue steer interrupt].include?(options[:input_behavior].to_s) ? options[:input_behavior].to_s : "queue"
|
|
173
174
|
@max_tokens = options[:max_tokens] || 16384
|
|
174
175
|
@verbose = options[:verbose] || false
|
|
175
176
|
@enable_compression = options[:enable_compression].nil? ? true : options[:enable_compression]
|
|
@@ -454,7 +455,7 @@ module Clacky
|
|
|
454
455
|
CONFIG_SETTINGS_KEYS = %w[
|
|
455
456
|
enable_compression enable_idle_compression enable_prompt_caching
|
|
456
457
|
compression_threshold message_count_threshold
|
|
457
|
-
memory_update_enabled
|
|
458
|
+
memory_update_enabled input_behavior
|
|
458
459
|
skill_evolution max_running_agents max_idle_agents
|
|
459
460
|
default_working_dir
|
|
460
461
|
proxy_url clacky_license_server
|
|
@@ -468,6 +469,7 @@ module Clacky
|
|
|
468
469
|
m.reject { |k, _| RUNTIME_ONLY_FIELDS.include?(k) }
|
|
469
470
|
end
|
|
470
471
|
settings = {
|
|
472
|
+
"input_behavior" => @input_behavior,
|
|
471
473
|
"enable_compression" => @enable_compression,
|
|
472
474
|
"enable_idle_compression" => @enable_idle_compression,
|
|
473
475
|
"enable_prompt_caching" => @enable_prompt_caching,
|
|
@@ -1161,7 +1163,7 @@ module Clacky
|
|
|
1161
1163
|
# find_by_base_url only matches registered preset URLs, so localhost
|
|
1162
1164
|
# and other non-openclacky endpoints correctly return nil.
|
|
1163
1165
|
provider_id = Clacky::Providers.find_by_base_url(m["base_url"])
|
|
1164
|
-
return nil unless provider_id ==
|
|
1166
|
+
return nil unless provider_id == Clacky::Providers::OPENCLACKY_ID
|
|
1165
1167
|
|
|
1166
1168
|
fallback_url = Clacky::Providers.fallback_base_url(provider_id)
|
|
1167
1169
|
return nil unless fallback_url
|
|
@@ -20,7 +20,7 @@ module Clacky
|
|
|
20
20
|
# list never drifts from providers.rb as models are added or removed.
|
|
21
21
|
def openclacky_models
|
|
22
22
|
@openclacky_models ||= begin
|
|
23
|
-
preset = Clacky::Providers.get(
|
|
23
|
+
preset = Clacky::Providers.get(Clacky::Providers::OPENCLACKY_ID)
|
|
24
24
|
return [] unless preset
|
|
25
25
|
OPENCLACKY_MODEL_KINDS.flat_map { |kind| preset[kind] || [] }.uniq
|
|
26
26
|
end
|
|
@@ -30,7 +30,7 @@ module Clacky
|
|
|
30
30
|
# carry an explicit provider id; legacy records (written before the
|
|
31
31
|
# field existed) are detected by matching the full openclacky model list.
|
|
32
32
|
def openclacky_record?(record)
|
|
33
|
-
return true if record.provider ==
|
|
33
|
+
return true if record.provider == Clacky::Providers::OPENCLACKY_ID
|
|
34
34
|
openclacky_models.include?(record.model.to_s)
|
|
35
35
|
end
|
|
36
36
|
|
|
@@ -15,11 +15,18 @@ module Clacky
|
|
|
15
15
|
# alias → real upstream model id, used to translate the model filter
|
|
16
16
|
# before querying the platform API. Vertex ids are preferred as the
|
|
17
17
|
# "primary" id where the gateway can dispatch an alias two ways.
|
|
18
|
+
# Retired aliases stay here so usage recorded under them still resolves.
|
|
18
19
|
ALIAS_TO_REAL = {
|
|
19
20
|
# deepseek (dsk-)
|
|
21
|
+
"dsk-deepseek-flash" => "deepseek-flash",
|
|
20
22
|
"dsk-deepseek-v4-pro" => "deepseek-v4-pro",
|
|
21
23
|
"dsk-deepseek-v4-flash" => "deepseek-v4-flash",
|
|
22
24
|
"dsk-deepseek-v4-flash-vision-exp" => "deepseek-v4-flash-vision-exp",
|
|
25
|
+
# gpt via bedrock (abs-, OpenAI-compatible endpoint)
|
|
26
|
+
"abs-gpt-6-astra" => "global.openai.gpt-6-astra",
|
|
27
|
+
"abs-gpt-5.6-sol" => "global.openai.gpt-5.6-sol",
|
|
28
|
+
"abs-gpt-5.6-terra" => "global.openai.gpt-5.6-terra",
|
|
29
|
+
"abs-gpt-5.6-luna" => "global.openai.gpt-5.6-luna",
|
|
23
30
|
# claude via bedrock (abs-)
|
|
24
31
|
"abs-claude-fable-5-1" => "global.anthropic.claude-fable-5-1",
|
|
25
32
|
"abs-claude-fable-5" => "global.anthropic.claude-fable-5",
|
data/lib/clacky/brand_config.rb
CHANGED
|
@@ -53,7 +53,7 @@ module Clacky
|
|
|
53
53
|
attr_reader :product_name, :package_name, :license_key, :license_activated_at,
|
|
54
54
|
:license_expires_at, :license_last_heartbeat, :device_id,
|
|
55
55
|
:logo_url, :support_contact, :license_user_id,
|
|
56
|
-
:support_qr_url, :theme_color,
|
|
56
|
+
:support_qr_url, :theme_color,
|
|
57
57
|
:distribution_last_refreshed_at, :license_last_heartbeat_failure
|
|
58
58
|
|
|
59
59
|
def initialize(attrs = {})
|
|
@@ -856,7 +856,7 @@ module Clacky
|
|
|
856
856
|
# Requires an activated license. Returns { success:, extensions: [], error: }.
|
|
857
857
|
# Each extension carries name + latest_version.download_url so
|
|
858
858
|
# install_brand_extension! can consume it directly.
|
|
859
|
-
def fetch_brand_extensions!
|
|
859
|
+
def fetch_brand_extensions!(sort: nil)
|
|
860
860
|
return { success: false, error: "License not activated", extensions: [] } unless activated?
|
|
861
861
|
|
|
862
862
|
user_id = parse_user_id_from_key(@license_key)
|
|
@@ -872,6 +872,7 @@ module Clacky
|
|
|
872
872
|
nonce: nonce,
|
|
873
873
|
signature: OpenSSL::HMAC.hexdigest("SHA256", @license_key, message)
|
|
874
874
|
}
|
|
875
|
+
payload[:sort] = sort if sort
|
|
875
876
|
|
|
876
877
|
response = api_post("/api/v1/licenses/extensions", payload)
|
|
877
878
|
|
|
@@ -1289,9 +1290,11 @@ module Clacky
|
|
|
1289
1290
|
|
|
1290
1291
|
# Synchronise brand skills in the background.
|
|
1291
1292
|
#
|
|
1292
|
-
# Fetches the remote skills list and installs
|
|
1293
|
-
#
|
|
1294
|
-
#
|
|
1293
|
+
# Fetches the remote skills list and installs every skill that is missing
|
|
1294
|
+
# locally or has a newer remote version — skills the brand publishes after
|
|
1295
|
+
# this install was activated therefore arrive without user action. The work
|
|
1296
|
+
# runs in a daemon Thread so it never blocks the caller (typically Agent
|
|
1297
|
+
# startup).
|
|
1295
1298
|
#
|
|
1296
1299
|
# If the license is not activated the method returns immediately without
|
|
1297
1300
|
# spawning a thread.
|
|
@@ -1299,10 +1302,7 @@ module Clacky
|
|
|
1299
1302
|
# @param on_complete [Proc, nil] Optional callback called with the sync
|
|
1300
1303
|
# results array once all downloads finish (useful for tests / UI feedback).
|
|
1301
1304
|
# @return [Thread, nil] The background thread, or nil if skipped.
|
|
1302
|
-
|
|
1303
|
-
# were previously installed. Used on first activation so every brand skill is
|
|
1304
|
-
# available immediately without the user clicking Install one-by-one.
|
|
1305
|
-
def sync_brand_skills_async!(on_complete: nil, install_new: false)
|
|
1305
|
+
def sync_brand_skills_async!(on_complete: nil)
|
|
1306
1306
|
return nil unless activated?
|
|
1307
1307
|
return nil if ENV["CLACKY_TEST"] == "1"
|
|
1308
1308
|
|
|
@@ -1321,16 +1321,13 @@ module Clacky
|
|
|
1321
1321
|
delete_brand_skill!(local_name) unless remote_skill_names.include?(local_name)
|
|
1322
1322
|
end
|
|
1323
1323
|
|
|
1324
|
-
#
|
|
1325
|
-
#
|
|
1326
|
-
#
|
|
1327
|
-
# installed explicitly by the user from the Brand Skills panel.
|
|
1324
|
+
# Install every remote skill that is missing locally or has a newer
|
|
1325
|
+
# version available, so the local catalogue converges on the brand's
|
|
1326
|
+
# without the user clicking Install in the Brand Skills panel.
|
|
1328
1327
|
installed = installed_brand_skills
|
|
1329
|
-
skills_to_install =
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
result[:skills].select { |s| s["needs_update"] }
|
|
1333
|
-
end
|
|
1328
|
+
skills_to_install = result[:skills].select do |s|
|
|
1329
|
+
s["needs_update"] || !installed.key?(s["name"])
|
|
1330
|
+
end
|
|
1334
1331
|
results = skills_to_install.map do |skill_info|
|
|
1335
1332
|
install_brand_skill!(skill_info)
|
|
1336
1333
|
end
|
|
@@ -1649,6 +1646,17 @@ module Clacky
|
|
|
1649
1646
|
# Non-fatal — metadata write failure should not break the upload flow
|
|
1650
1647
|
end
|
|
1651
1648
|
|
|
1649
|
+
# Vendors may configure a bare domain ("example.com"). Without a scheme the
|
|
1650
|
+
# browser treats it as a relative path and resolves it against the local
|
|
1651
|
+
# server, so normalize on read to cover every consumer at once.
|
|
1652
|
+
def homepage_url
|
|
1653
|
+
raw = @homepage_url.to_s.strip
|
|
1654
|
+
return nil if raw.empty?
|
|
1655
|
+
return raw if raw.match?(%r{\A[a-z][a-z0-9+.\-]*:}i)
|
|
1656
|
+
|
|
1657
|
+
"https://#{raw}"
|
|
1658
|
+
end
|
|
1659
|
+
|
|
1652
1660
|
# Returns a hash representation for JSON serialization (e.g. /api/brand).
|
|
1653
1661
|
def to_h
|
|
1654
1662
|
{
|
|
@@ -1658,7 +1666,7 @@ module Clacky
|
|
|
1658
1666
|
support_contact: @support_contact,
|
|
1659
1667
|
support_qr_url: @support_qr_url,
|
|
1660
1668
|
theme_color: @theme_color,
|
|
1661
|
-
homepage_url:
|
|
1669
|
+
homepage_url: homepage_url,
|
|
1662
1670
|
branded: branded?,
|
|
1663
1671
|
activated: activated?,
|
|
1664
1672
|
expired: expired?,
|
data/lib/clacky/cli.rb
CHANGED
|
@@ -345,6 +345,29 @@ module Clacky
|
|
|
345
345
|
ui_controller.show_success("Switched to model: #{config.model_name}")
|
|
346
346
|
end
|
|
347
347
|
|
|
348
|
+
# Handle the `/think` slash command — pick the reasoning effort level
|
|
349
|
+
# for the current session. "off" normalizes to nil (provider default),
|
|
350
|
+
# matching the Web UI's reasoning_effort switcher semantics.
|
|
351
|
+
private def handle_think_command(ui_controller, agent, session_manager = nil)
|
|
352
|
+
choice = ui_controller.show_reasoning_effort_menu(agent.reasoning_effort)
|
|
353
|
+
return if choice.nil?
|
|
354
|
+
|
|
355
|
+
agent.reasoning_effort = choice
|
|
356
|
+
|
|
357
|
+
# The override lives in the session file (not config.yml), so persist
|
|
358
|
+
# it now — otherwise it would be lost if the user quits before the
|
|
359
|
+
# next task.
|
|
360
|
+
session_manager&.save(agent.to_session_data(updated_at: Time.now))
|
|
361
|
+
|
|
362
|
+
# Reflect the change in the session bar (appended after the model name)
|
|
363
|
+
ui_controller.config[:reasoning_effort] = agent.reasoning_effort
|
|
364
|
+
ui_controller.update_sessionbar
|
|
365
|
+
|
|
366
|
+
current = agent.reasoning_effort
|
|
367
|
+
message = current ? "Thinking level set to #{current}" : "Thinking level: off (provider default)"
|
|
368
|
+
ui_controller.show_success(message)
|
|
369
|
+
end
|
|
370
|
+
|
|
348
371
|
private def handle_time_machine_command(ui_controller, agent, session_manager)
|
|
349
372
|
# Get task history from agent
|
|
350
373
|
history = agent.get_task_history(limit: 10)
|
|
@@ -874,6 +897,7 @@ module Clacky
|
|
|
874
897
|
working_dir: working_dir,
|
|
875
898
|
mode: agent_config.permission_mode.to_s,
|
|
876
899
|
model: agent_config.model_name,
|
|
900
|
+
reasoning_effort: agent.reasoning_effort,
|
|
877
901
|
theme: theme_name
|
|
878
902
|
)
|
|
879
903
|
end
|
|
@@ -889,6 +913,7 @@ module Clacky
|
|
|
889
913
|
|
|
890
914
|
# Track current working thread (agent or idle compression that can be interrupted)
|
|
891
915
|
current_task_thread = nil
|
|
916
|
+
input_submission_mutex = Mutex.new
|
|
892
917
|
shutting_down = false
|
|
893
918
|
|
|
894
919
|
# Idle compression timer - triggers compression after 180s of inactivity
|
|
@@ -988,7 +1013,26 @@ module Clacky
|
|
|
988
1013
|
end
|
|
989
1014
|
|
|
990
1015
|
# Set up input handler
|
|
991
|
-
ui_controller.
|
|
1016
|
+
if ui_controller.respond_to?(:queue_input_while_running=)
|
|
1017
|
+
ui_controller.queue_input_while_running = -> { current_task_thread&.alive? && %w[queue steer].include?(agent_config.input_behavior) }
|
|
1018
|
+
end
|
|
1019
|
+
if ui_controller.respond_to?(:input_area)
|
|
1020
|
+
ui_controller.input_area.guidance_lines_provider = -> { ui_controller.guidance_lines }
|
|
1021
|
+
end
|
|
1022
|
+
ui_controller.show_input_queue(agent.pending_inputs) if ui_controller.respond_to?(:show_input_queue)
|
|
1023
|
+
handle_input = lambda do |input, files, display: nil, force_interrupt: false|
|
|
1024
|
+
if input.to_s.match?(%r{\A/input-mode(?:\s|\z)})
|
|
1025
|
+
mode = input.split[1]
|
|
1026
|
+
if %w[queue interrupt steer].include?(mode)
|
|
1027
|
+
agent_config.input_behavior = mode
|
|
1028
|
+
agent_config.save
|
|
1029
|
+
ui_controller.show_info("Input mode: #{mode}")
|
|
1030
|
+
else
|
|
1031
|
+
ui_controller.show_info("Current input mode: #{agent_config.input_behavior}\n/input-mode queue · /input-mode steer · /input-mode interrupt")
|
|
1032
|
+
end
|
|
1033
|
+
next
|
|
1034
|
+
end
|
|
1035
|
+
|
|
992
1036
|
# Handle commands
|
|
993
1037
|
case input.downcase.strip
|
|
994
1038
|
when "/config"
|
|
@@ -997,6 +1041,9 @@ module Clacky
|
|
|
997
1041
|
when "/model"
|
|
998
1042
|
handle_model_command(ui_controller, agent_config, agent, session_manager)
|
|
999
1043
|
next
|
|
1044
|
+
when "/think"
|
|
1045
|
+
handle_think_command(ui_controller, agent, session_manager)
|
|
1046
|
+
next
|
|
1000
1047
|
when "/undo"
|
|
1001
1048
|
handle_time_machine_command(ui_controller, agent, session_manager)
|
|
1002
1049
|
next
|
|
@@ -1040,13 +1087,29 @@ module Clacky
|
|
|
1040
1087
|
next
|
|
1041
1088
|
end
|
|
1042
1089
|
|
|
1090
|
+
queued = input_submission_mutex.synchronize do
|
|
1091
|
+
if !force_interrupt && current_task_thread&.alive? && %w[queue steer].include?(agent_config.input_behavior)
|
|
1092
|
+
agent.enqueue_input(input, delivery: agent_config.input_behavior.to_sym, files: files, created_at: Time.now.to_f)
|
|
1093
|
+
true
|
|
1094
|
+
end
|
|
1095
|
+
end
|
|
1096
|
+
next if queued
|
|
1097
|
+
|
|
1043
1098
|
# If any task thread is running, interrupt it first
|
|
1044
1099
|
if current_task_thread&.alive?
|
|
1045
1100
|
current_task_thread.raise(Clacky::AgentInterrupted, "New input received")
|
|
1046
|
-
current_task_thread.join(2)
|
|
1101
|
+
unless current_task_thread.join(2)
|
|
1102
|
+
agent.enqueue_input(input, files: files, created_at: Time.now.to_f)
|
|
1103
|
+
ui_controller.show_info("Current task is still stopping. Your message remains queued.")
|
|
1104
|
+
next
|
|
1105
|
+
end
|
|
1047
1106
|
ui_controller.set_idle_status
|
|
1048
1107
|
end
|
|
1049
1108
|
|
|
1109
|
+
if force_interrupt
|
|
1110
|
+
ui_controller.show_user_message(input, files: files, steering: true)
|
|
1111
|
+
end
|
|
1112
|
+
|
|
1050
1113
|
# Cancel idle timer if running (new input means user is active)
|
|
1051
1114
|
idle_timer.cancel
|
|
1052
1115
|
|
|
@@ -1061,14 +1124,19 @@ module Clacky
|
|
|
1061
1124
|
# Run agent (Agent will call @ui methods directly)
|
|
1062
1125
|
# Agent internally tracks total_tasks and total_cost
|
|
1063
1126
|
result = agent.run(input, files: files)
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1127
|
+
loop do
|
|
1128
|
+
if session_manager
|
|
1129
|
+
session_manager.save(agent.to_session_data(status: :success, updated_at: Time.now))
|
|
1130
|
+
end
|
|
1131
|
+
ui_controller.update_sessionbar(tasks: agent.total_tasks, cost: agent.total_cost)
|
|
1132
|
+
pending = input_submission_mutex.synchronize do
|
|
1133
|
+
entry = agent.take_pending_input if Agent.task_completed?(result)
|
|
1134
|
+
current_task_thread = nil unless entry
|
|
1135
|
+
entry
|
|
1136
|
+
end
|
|
1137
|
+
break unless pending
|
|
1138
|
+
result = agent.run_pending_input(pending)
|
|
1068
1139
|
end
|
|
1069
|
-
|
|
1070
|
-
# Update session bar with agent's cumulative stats
|
|
1071
|
-
ui_controller.update_sessionbar(tasks: agent.total_tasks, cost: agent.total_cost)
|
|
1072
1140
|
rescue Clacky::AgentInterrupted, StandardError => e
|
|
1073
1141
|
begin
|
|
1074
1142
|
handle_agent_exception(ui_controller, agent, session_manager, e)
|
|
@@ -1078,13 +1146,25 @@ module Clacky
|
|
|
1078
1146
|
$stderr.puts "[cli] handle_agent_exception failed: #{ex.class}: #{ex.message}"
|
|
1079
1147
|
end
|
|
1080
1148
|
ensure
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1149
|
+
input_submission_mutex.synchronize do
|
|
1150
|
+
current_task_thread = nil if current_task_thread == Thread.current
|
|
1151
|
+
idle_timer.start unless shutting_down || current_task_thread&.alive?
|
|
1152
|
+
end
|
|
1084
1153
|
end
|
|
1085
1154
|
end
|
|
1086
1155
|
end
|
|
1087
1156
|
|
|
1157
|
+
ui_controller.on_input do |input, files, display: nil|
|
|
1158
|
+
handle_input.call(input, files, display: display)
|
|
1159
|
+
end
|
|
1160
|
+
ui_controller.on_guidance_action do |action, id|
|
|
1161
|
+
# Claim the displayed message once; an already-consumed ID is a no-op.
|
|
1162
|
+
entry = agent.remove_pending_input(id)
|
|
1163
|
+
if entry && action == :send_now
|
|
1164
|
+
handle_input.call(entry[:content], entry.dig(:options, :files) || [], force_interrupt: true)
|
|
1165
|
+
end
|
|
1166
|
+
end if ui_controller.respond_to?(:on_guidance_action)
|
|
1167
|
+
|
|
1088
1168
|
# Initialize UI screen first
|
|
1089
1169
|
if is_session_load
|
|
1090
1170
|
recent_user_messages = agent.get_recent_user_messages(limit: 5)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Clacky
|
|
4
|
+
# Pending guidance belongs to the fixed composer area, not the transcript.
|
|
5
|
+
module CliGuidance
|
|
6
|
+
def on_guidance_action(&block)
|
|
7
|
+
@guidance_action = block
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def request_guidance_action(action)
|
|
11
|
+
entry = (@guidance_entries || []).last
|
|
12
|
+
return false unless entry && @guidance_action
|
|
13
|
+
dispatch_guidance_action(action, entry[:id])
|
|
14
|
+
true
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def dispatch_guidance_action(action, id)
|
|
18
|
+
@guidance_action.call(action, id)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def guidance_control?(text)
|
|
22
|
+
text.to_s.match?(%r{\A/input-mode(?:\s|\z)})
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def guidance_deferred?(text)
|
|
26
|
+
guidance_control?(text) || !!queue_input_while_running&.call
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def guidance_lines
|
|
30
|
+
entries = @guidance_entries || []
|
|
31
|
+
return [] if entries.empty?
|
|
32
|
+
|
|
33
|
+
previews = entries.last(3).map do |entry|
|
|
34
|
+
text = entry[:content].to_s
|
|
35
|
+
if text.strip.empty?
|
|
36
|
+
text = Array(entry.dig(:options, :files)).map { |f| f[:name] || f["name"] }.compact.join(", ")
|
|
37
|
+
end
|
|
38
|
+
" · #{text.gsub(/[[:cntrl:]]/, ' ').gsub(/\s+/, ' ').strip}"
|
|
39
|
+
end
|
|
40
|
+
["Pending · Ctrl+D delete last · Ctrl+G send now", *(entries.size > 3 ? [" … #{entries.size - 3} earlier"] : []), *previews]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def show_input_queue(entries)
|
|
44
|
+
@guidance_entries = entries
|
|
45
|
+
refresh_guidance if respond_to?(:refresh_guidance)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
data/lib/clacky/client.rb
CHANGED
|
@@ -720,12 +720,12 @@ module Clacky
|
|
|
720
720
|
conn.headers["x-api-key"] = @api_key
|
|
721
721
|
conn.headers["anthropic-version"] = "2023-06-01"
|
|
722
722
|
conn.headers["anthropic-dangerous-direct-browser-access"] = "true"
|
|
723
|
-
if @provider_id ==
|
|
723
|
+
if @provider_id == Clacky::Providers::OPENROUTER_ID
|
|
724
724
|
conn.headers["Authorization"] = "Bearer #{@api_key}"
|
|
725
725
|
end
|
|
726
726
|
# Moonshot's Kimi Code (Coding Plan) endpoint enforces a User-Agent
|
|
727
727
|
# prefix whitelist limited to first-party coding agents.
|
|
728
|
-
if @provider_id ==
|
|
728
|
+
if @provider_id == Clacky::Providers::KIMI_CODING_ID
|
|
729
729
|
conn.headers["User-Agent"] = "claude-cli/1.0.51 (external, cli)"
|
|
730
730
|
end
|
|
731
731
|
conn.options.timeout = @read_timeout || 300
|