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.
Files changed (87) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +96 -0
  3. data/lib/clacky/agent/chunk_index.rb +83 -0
  4. data/lib/clacky/agent/history_navigation.rb +239 -0
  5. data/lib/clacky/agent/session_serializer.rb +53 -98
  6. data/lib/clacky/agent.rb +477 -297
  7. data/lib/clacky/agent_config.rb +5 -3
  8. data/lib/clacky/billing/billing_store.rb +2 -2
  9. data/lib/clacky/billing/platform_billing.rb +7 -0
  10. data/lib/clacky/brand_config.rb +27 -19
  11. data/lib/clacky/cli.rb +92 -12
  12. data/lib/clacky/cli_guidance.rb +48 -0
  13. data/lib/clacky/client.rb +2 -2
  14. data/lib/clacky/default_extensions/ext-studio/agents/ext-developer/system_prompt.md +70 -134
  15. data/lib/clacky/default_extensions/ext-studio/api/handler.rb +4 -1
  16. data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +307 -98
  17. data/lib/clacky/default_extensions/ext-studio/skills/ext-develop/SKILL.md +179 -577
  18. data/lib/clacky/default_extensions/git/panels/git/view.js +34 -14
  19. data/lib/clacky/default_extensions/preview/ext.yml +20 -0
  20. data/lib/clacky/default_extensions/preview/panels/preview/view.js +475 -0
  21. data/lib/clacky/default_extensions/time_machine/panels/time_machine/view.js +1 -10
  22. data/lib/clacky/extension/api_extension.rb +18 -6
  23. data/lib/clacky/extension/verifier.rb +1 -1
  24. data/lib/clacky/json_ui_controller.rb +4 -2
  25. data/lib/clacky/media/openai_compat.rb +70 -30
  26. data/lib/clacky/message_format/bedrock.rb +6 -1
  27. data/lib/clacky/message_format/open_ai.rb +22 -5
  28. data/lib/clacky/message_format/open_ai_responses.rb +7 -3
  29. data/lib/clacky/plain_ui_controller.rb +1 -1
  30. data/lib/clacky/prompts/base.md +1 -1
  31. data/lib/clacky/providers.rb +44 -13
  32. data/lib/clacky/rich_ui/components/composer_guidance.rb +49 -0
  33. data/lib/clacky/rich_ui/rich_ui_controller.rb +24 -4
  34. data/lib/clacky/rich_ui/shell/rich_agent_shell.rb +13 -0
  35. data/lib/clacky/search_config.rb +3 -3
  36. data/lib/clacky/server/channel/channel_manager.rb +25 -7
  37. data/lib/clacky/server/channel/channel_ui_controller.rb +1 -1
  38. data/lib/clacky/server/dir_picker.rb +67 -4
  39. data/lib/clacky/server/git_panel.rb +10 -2
  40. data/lib/clacky/server/http_server.rb +391 -75
  41. data/lib/clacky/server/preview.rb +351 -0
  42. data/lib/clacky/server/session_registry.rb +4 -0
  43. data/lib/clacky/server/web_ui_controller.rb +10 -5
  44. data/lib/clacky/session_manager.rb +52 -1
  45. data/lib/clacky/skill.rb +48 -0
  46. data/lib/clacky/tools/browser.rb +176 -19
  47. data/lib/clacky/tools/web_search.rb +91 -8
  48. data/lib/clacky/ui2/components/command_suggestions.rb +3 -1
  49. data/lib/clacky/ui2/components/input_area.rb +37 -4
  50. data/lib/clacky/ui2/components/modal_component.rb +35 -6
  51. data/lib/clacky/ui2/screen_buffer.rb +1 -0
  52. data/lib/clacky/ui2/ui_controller.rb +72 -12
  53. data/lib/clacky/ui_interface.rb +1 -1
  54. data/lib/clacky/utils/file_processor.rb +19 -7
  55. data/lib/clacky/utils/mac_app_detector.rb +186 -0
  56. data/lib/clacky/utils/model_pricing.rb +118 -68
  57. data/lib/clacky/utils/windows_app_detector.rb +334 -0
  58. data/lib/clacky/version.rb +1 -1
  59. data/lib/clacky/web/app.css +998 -168
  60. data/lib/clacky/web/app.js +19 -0
  61. data/lib/clacky/web/components/chat-navigator.js +489 -202
  62. data/lib/clacky/web/components/code-editor.js +191 -5
  63. data/lib/clacky/web/components/composer.js +3 -1
  64. data/lib/clacky/web/components/datepicker.js +19 -19
  65. data/lib/clacky/web/components/mentions.js +11 -14
  66. data/lib/clacky/web/components/model-picker.js +66 -23
  67. data/lib/clacky/web/components/quote-select.js +341 -0
  68. data/lib/clacky/web/core/aside.js +157 -8
  69. data/lib/clacky/web/core/ext.js +16 -0
  70. data/lib/clacky/web/features/billing/view.js +75 -21
  71. data/lib/clacky/web/features/extensions/store.js +4 -1
  72. data/lib/clacky/web/features/model-tester/store.js +37 -4
  73. data/lib/clacky/web/features/skills/store.js +18 -1
  74. data/lib/clacky/web/features/skills/view.js +128 -7
  75. data/lib/clacky/web/features/workspace/store.js +80 -7
  76. data/lib/clacky/web/features/workspace/view.js +474 -43
  77. data/lib/clacky/web/i18n.js +158 -23
  78. data/lib/clacky/web/index.html +57 -29
  79. data/lib/clacky/web/sessions.js +517 -127
  80. data/lib/clacky/web/settings.js +155 -6
  81. data/lib/clacky/web/utils.js +34 -0
  82. data/lib/clacky/web/vendor/codemirror/codemirror.min.js +24 -19
  83. data/lib/clacky/web/vendor/codemirror/entry.js +130 -0
  84. data/lib/clacky/web/vendor/codemirror/package.json +29 -0
  85. data/lib/clacky/web/ws-dispatcher.js +161 -4
  86. data/lib/clacky.rb +2 -0
  87. metadata +13 -1
@@ -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 == "openclacky"
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("openclacky")
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 == "openclacky"
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",
@@ -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, :homepage_url,
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 any skill whose remote version
1293
- # differs from the locally installed version. The work runs in a daemon
1294
- # Thread so it never blocks the caller (typically Agent startup).
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
- # install_new: when true, install ALL remote skills regardless of whether they
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
- # When install_new is true (e.g. on first activation) install every
1325
- # remote skill. Otherwise limit auto-sync to skills already installed
1326
- # locally that have a newer version available — new skills must be
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 = if install_new
1330
- result[:skills].select { |s| s["needs_update"] || !installed.key?(s["name"]) }
1331
- else
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: @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.on_input do |input, files, display: nil|
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) # Wait up to 2 seconds for graceful shutdown
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
- # Save session after each task
1066
- if session_manager
1067
- session_manager.save(agent.to_session_data(status: :success, updated_at: Time.now))
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
- current_task_thread = nil
1082
- # Start idle timer after agent completes
1083
- idle_timer.start unless shutting_down
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 == "openrouter"
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 == "kimi-coding"
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