openclacky 1.2.9 → 1.2.10

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b2060d694267d0947681785d2e2ffe730d0b241a9ac2ec68e218eb037478bf27
4
- data.tar.gz: 0b3e301010e16752da0bd64a9603b06010c2a23c5bd81884c7719d74f8f1bd67
3
+ metadata.gz: 0b32940868f1d61791afd615ff73dbaf72dc80c111f9f9435ef939ef39ae5dec
4
+ data.tar.gz: be8efa7ee318c3f174ddbbdf1f5b2754705eb6a5d3f263aa11cbe5539b198e8f
5
5
  SHA512:
6
- metadata.gz: 386e2359d904b9a6bc81e56429cd585aaef59b7174f5dbc93e51cdd2bf97df703fd8145910759f50427632fcc90a307ed3ec6b19e48f0230d7e0c39987d3e7a8
7
- data.tar.gz: 15413b83259ef7a39acac101597149cbf2144473da691d885f14d3b271076399da14c924db19201a1b99d8bf264542b56f3619b6cb6b903dfdac462e46f15a97
6
+ metadata.gz: abcbed799ca8feed1a41e39a72bd8e6a5e9184c8e76a67bac79f9bee07f88ebf6b639102d6cfcd6527bf80b7b3c9bda5665f131bf81a164b9a14b963cad1ea47
7
+ data.tar.gz: 829bc77c06483853c1d568d04a528a9611c6e7f775b38b6c6fdebe04c5a5ae6974328bb1ac9c0f8cb1afe0765a60795061157b65860eaf26e8bb7368dadb2b8e
data/CHANGELOG.md CHANGED
@@ -5,6 +5,29 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.2.10] - 2026-06-03
9
+
10
+ ### Added
11
+ - Compressor concurrency config and Ollama context size auto-detection
12
+ - Channel sessions auto-rebind on server restart
13
+ - Disk space check (4 GB minimum) on Windows install
14
+ - WSL network connectivity pre-check before installation
15
+ - MiniMax M3 provider with vision support and pricing
16
+ - One-click exchange rate update in settings
17
+ - Rich TUI controller for terminal interaction
18
+
19
+ ### Improved
20
+ - WebUI working directory selector UX
21
+ - Qwen pricing table: official rates, promo discounts, clean up stale models
22
+
23
+ ### Fixed
24
+ - Directory picker interaction and indentation issues
25
+ - File upload for PDF and Excel files
26
+ - Session soft-delete: count-based cleanup and never evict pinned sessions
27
+ - Usage tooltip total value showing unreadable blue background
28
+ - Workspace file list now collapses on session switch
29
+ - Install script network errors use exit code 2 for better error handling
30
+
8
31
  ## [1.2.9] - 2026-06-01
9
32
 
10
33
  ### Added
@@ -583,6 +583,9 @@ module Clacky
583
583
  "exceeds the maximum context", # Portkey & generic gateways
584
584
  "exceeds the model's context", # Generic
585
585
  "exceeds the model's maximum", # Generic
586
+ "exceeds the available context", # llama.cpp / llama-server
587
+ "available context size", # llama.cpp / llama-server variant
588
+ "try increasing it", # llama.cpp action hint (server.cpp)
586
589
  "reduce the length of the input", # Qwen action hint
587
590
  "reduce the length of the messages", # OpenAI action hint
588
591
  "reduce the length of your", # Generic action hint
@@ -5,9 +5,10 @@ module Clacky
5
5
  # Message compression functionality for managing conversation history
6
6
  # Handles automatic compression when token limits are exceeded
7
7
  module MessageCompressorHelper
8
- # Compression thresholds
9
- COMPRESSION_THRESHOLD = 150_000 # Trigger compression when exceeding this (in tokens)
10
- MESSAGE_COUNT_THRESHOLD = 200 # Trigger compression when exceeding this (in message count)
8
+ # Compression behavior knobs.
9
+ # Token & message-count thresholds are owned by AgentConfig — see
10
+ # AgentConfig::DEFAULT_COMPRESSION_THRESHOLD / DEFAULT_MESSAGE_COUNT_THRESHOLD.
11
+ # The constants below are tuning parameters not currently exposed as user config.
11
12
  MAX_RECENT_MESSAGES = 20 # Keep this many recent message pairs intact
12
13
  TARGET_COMPRESSED_TOKENS = 10_000 # Target size after compression
13
14
  IDLE_COMPRESSION_THRESHOLD = 20_000 # Minimum messages needed for idle compression
@@ -141,8 +142,8 @@ module Clacky
141
142
  else
142
143
  # Normal compression - check thresholds
143
144
  # Either: token count exceeds threshold OR message count exceeds threshold
144
- token_threshold_exceeded = total_tokens >= COMPRESSION_THRESHOLD
145
- message_count_exceeded = message_count >= MESSAGE_COUNT_THRESHOLD
145
+ token_threshold_exceeded = total_tokens >= @config.compression_threshold
146
+ message_count_exceeded = message_count >= @config.message_count_threshold
146
147
 
147
148
  # Only compress if we exceed at least one threshold
148
149
  return nil unless token_threshold_exceeded || message_count_exceeded
@@ -25,6 +25,9 @@ module Clacky
25
25
  # Restore source; fall back to :manual for sessions saved before this field existed
26
26
  @source = (session_data[:source] || "manual").to_sym
27
27
 
28
+ # Restore channel info for IM platform sessions
29
+ @channel_info = session_data[:channel_info]
30
+
28
31
  # Restore cache statistics if available
29
32
  @cache_stats = session_data.dig(:stats, :cache_stats) || {
30
33
  cache_creation_input_tokens: 0,
@@ -158,6 +161,7 @@ module Clacky
158
161
  model_base_url: persisted_card_field("base_url"),
159
162
  sub_model: @config.session_model_overlay_name
160
163
  },
164
+ channel_info: @channel_info,
161
165
  stats: stats_data,
162
166
  messages: @history.to_a
163
167
  }
data/lib/clacky/agent.rb CHANGED
@@ -46,6 +46,7 @@ module Clacky
46
46
  :latest_latency, # Hash of latency metrics from the most recent LLM call (see Client#send_messages_with_tools)
47
47
  :reasoning_effort
48
48
  attr_accessor :pinned
49
+ attr_accessor :channel_info
49
50
 
50
51
  REASONING_EFFORTS = %w[low medium high].freeze
51
52
 
@@ -72,6 +73,7 @@ module Clacky
72
73
  @config = config.is_a?(AgentConfig) ? config : AgentConfig.new(config)
73
74
  @agent_profile = AgentProfile.load(profile)
74
75
  @source = source.to_sym # :manual | :cron | :channel
76
+ @channel_info = nil # { platform:, user_id:, user_name:, chat_id: } set by ChannelManager
75
77
  @tool_registry = ToolRegistry.new
76
78
  @hooks = HookManager.new
77
79
  @session_id = session_id
@@ -1593,6 +1595,13 @@ module Clacky
1593
1595
  desktop ? "Desktop: #{desktop}" : nil,
1594
1596
  "Working directory: #{@working_dir}"
1595
1597
  ].compact.join(". ")
1598
+ if @channel_info
1599
+ platform = @channel_info[:platform].to_s
1600
+ user_id = @channel_info[:user_id].to_s
1601
+ user_name = @channel_info[:user_name].to_s
1602
+ sender = user_name.empty? ? user_id : "@#{user_name}(#{user_id})"
1603
+ parts = "#{parts}. Channel: #{platform}, Sender: #{sender}"
1604
+ end
1596
1605
 
1597
1606
  content = "[Session context: #{parts}]"
1598
1607
 
@@ -151,8 +151,16 @@ module Clacky
151
151
 
152
152
  PERMISSION_MODES = [:auto_approve, :confirm_safes, :confirm_all].freeze
153
153
 
154
+ # Conversation-history compression defaults. Both are user-configurable
155
+ # via `settings.compression_threshold` / `settings.message_count_threshold`
156
+ # in ~/.clacky/config.yml — local-model users (llama.cpp / ollama / vllm)
157
+ # typically lower compression_threshold to fit their server's context window.
158
+ DEFAULT_COMPRESSION_THRESHOLD = 150_000
159
+ DEFAULT_MESSAGE_COUNT_THRESHOLD = 200
160
+
154
161
  attr_accessor :permission_mode, :max_tokens, :verbose,
155
162
  :enable_compression, :enable_prompt_caching,
163
+ :compression_threshold, :message_count_threshold,
156
164
  :models, :current_model_index, :current_model_id,
157
165
  :memory_update_enabled, :skill_evolution,
158
166
  :max_running_agents, :max_idle_agents,
@@ -165,6 +173,13 @@ module Clacky
165
173
  @enable_compression = options[:enable_compression].nil? ? true : options[:enable_compression]
166
174
  # Enable prompt caching by default for cost savings
167
175
  @enable_prompt_caching = options[:enable_prompt_caching].nil? ? true : options[:enable_prompt_caching]
176
+ # Token threshold that triggers proactive history compression. Local models
177
+ # (llama.cpp, ollama, vllm) often have small context windows (e.g. 64k);
178
+ # users with such setups should lower this to avoid hitting the server-side ceiling.
179
+ @compression_threshold = options[:compression_threshold] || DEFAULT_COMPRESSION_THRESHOLD
180
+ # Message-count threshold that also triggers compression, independent of token count.
181
+ # Guards against pathological histories with many tiny messages.
182
+ @message_count_threshold = options[:message_count_threshold] || DEFAULT_MESSAGE_COUNT_THRESHOLD
168
183
 
169
184
  # Models configuration
170
185
  @models = options[:models] || []
@@ -387,7 +402,9 @@ module Clacky
387
402
  # Settings keys that are persisted to config.yml.
388
403
  # These map directly to AgentConfig accessors.
389
404
  CONFIG_SETTINGS_KEYS = %w[
390
- enable_compression enable_prompt_caching memory_update_enabled
405
+ enable_compression enable_prompt_caching
406
+ compression_threshold message_count_threshold
407
+ memory_update_enabled
391
408
  skill_evolution max_running_agents max_idle_agents
392
409
  default_working_dir
393
410
  ].freeze
@@ -402,6 +419,8 @@ module Clacky
402
419
  settings = {
403
420
  "enable_compression" => @enable_compression,
404
421
  "enable_prompt_caching" => @enable_prompt_caching,
422
+ "compression_threshold" => @compression_threshold,
423
+ "message_count_threshold" => @message_count_threshold,
405
424
  "memory_update_enabled" => @memory_update_enabled,
406
425
  "skill_evolution" => @skill_evolution,
407
426
  "max_running_agents" => @max_running_agents,
@@ -783,6 +783,7 @@ module Clacky
783
783
 
784
784
  FileUtils.rm_f(tmp_zip)
785
785
 
786
+
786
787
  record_installed_skill(slug, version, skill_info["description"],
787
788
  encrypted: encrypted,
788
789
  description_zh: skill_info["description_zh"],
data/lib/clacky/cli.rb CHANGED
@@ -53,6 +53,7 @@ module Clacky
53
53
  option :list, type: :boolean, aliases: "-l", desc: "List recent sessions"
54
54
  option :attach, type: :string, aliases: "-a", desc: "Attach to session by number or keyword"
55
55
  option :json, type: :boolean, default: false, desc: "Output NDJSON to stdout (for scripting/piping)"
56
+ option :ui, type: :string, default: nil, desc: "Interactive UI implementation: ui2, rich (default: ui2)"
56
57
  option :message, type: :string, aliases: "-m", desc: "Run non-interactively with this message and exit"
57
58
  option :file, type: :array, aliases: "-f", desc: "File path(s) to attach (use with -m; supports images and documents)"
58
59
  option :image, type: :array, aliases: "-i", desc: "Image file path(s) to attach (alias for --file, kept for compatibility)"
@@ -725,27 +726,47 @@ module Clacky
725
726
  # Brand license check — must happen before UI2 starts (raw terminal mode conflict)
726
727
  check_brand_license_cli
727
728
 
728
- # Detect terminal background BEFORE starting UI2 to avoid output interference
729
- is_dark_bg = UI2::TerminalDetector.detect_dark_background
729
+ ui_name = (options[:ui] || ENV["OPENCLACKY_UI"] || "ui2").to_s
730
730
 
731
- # Pass detected background mode to theme manager (singleton)
732
- UI2::ThemeManager.instance.set_background_mode(is_dark_bg)
731
+ ui_controller = if ui_name == "rich"
732
+ if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("2.6.0")
733
+ say "Error: Rich UI requires Ruby >= 2.6. Use --ui ui2 on Ruby #{RUBY_VERSION}.", :red
734
+ exit 1
735
+ end
736
+ require_relative "rich_ui_controller"
737
+ RichUIController.new(
738
+ working_dir: working_dir,
739
+ mode: agent_config.permission_mode.to_s,
740
+ model: agent_config.model_name,
741
+ theme: options[:theme]
742
+ )
743
+ else
744
+ unless ui_name == "ui2"
745
+ say "Error: Unknown UI '#{ui_name}'. Available UIs: ui2, rich", :red
746
+ exit 1
747
+ end
748
+ # Detect terminal background BEFORE starting UI2 to avoid output interference
749
+ is_dark_bg = UI2::TerminalDetector.detect_dark_background
733
750
 
734
- # Validate theme
735
- theme_name = options[:theme] || "hacker"
736
- available_themes = UI2::ThemeManager.available_themes.map(&:to_s)
737
- unless available_themes.include?(theme_name)
738
- say "Error: Unknown theme '#{theme_name}'. Available themes: #{available_themes.join(', ')}", :red
739
- exit 1
740
- end
751
+ # Pass detected background mode to theme manager (singleton)
752
+ UI2::ThemeManager.instance.set_background_mode(is_dark_bg)
741
753
 
742
- # Create UI2 controller with configuration
743
- ui_controller = UI2::UIController.new(
744
- working_dir: working_dir,
745
- mode: agent_config.permission_mode.to_s,
746
- model: agent_config.model_name,
747
- theme: theme_name
748
- )
754
+ # Validate theme
755
+ theme_name = options[:theme] || "hacker"
756
+ available_themes = UI2::ThemeManager.available_themes.map(&:to_s)
757
+ unless available_themes.include?(theme_name)
758
+ say "Error: Unknown theme '#{theme_name}'. Available themes: #{available_themes.join(', ')}", :red
759
+ exit 1
760
+ end
761
+
762
+ # Create UI2 controller with configuration
763
+ UI2::UIController.new(
764
+ working_dir: working_dir,
765
+ mode: agent_config.permission_mode.to_s,
766
+ model: agent_config.model_name,
767
+ theme: theme_name
768
+ )
769
+ end
749
770
 
750
771
  # Inject UI into agent
751
772
  agent.instance_variable_set(:@ui, ui_controller)
@@ -758,6 +779,7 @@ module Clacky
758
779
 
759
780
  # Track current working thread (agent or idle compression that can be interrupted)
760
781
  current_task_thread = nil
782
+ shutting_down = false
761
783
 
762
784
  # Idle compression timer - triggers compression after 180s of inactivity
763
785
  idle_timer = Clacky::IdleCompressionTimer.new(
@@ -816,7 +838,9 @@ module Clacky
816
838
  end
817
839
 
818
840
  # Stop UI and exit
819
- ui_controller.stop
841
+ shutting_down = true
842
+ idle_timer.shutdown
843
+ ui_controller.stop(clear_screen: true)
820
844
  exit(0)
821
845
  end
822
846
 
@@ -867,7 +891,9 @@ module Clacky
867
891
  ui_controller.update_todos([])
868
892
  next
869
893
  when "/exit", "/quit"
870
- ui_controller.stop
894
+ shutting_down = true
895
+ idle_timer.shutdown
896
+ ui_controller.stop(clear_screen: true)
871
897
  exit(0)
872
898
  when "/help"
873
899
  sleep 0.1
@@ -909,7 +935,7 @@ module Clacky
909
935
  ensure
910
936
  current_task_thread = nil
911
937
  # Start idle timer after agent completes
912
- idle_timer.start
938
+ idle_timer.start unless shutting_down
913
939
  end
914
940
  end
915
941
  end
@@ -928,7 +954,8 @@ module Clacky
928
954
  ui_controller.start_input_loop
929
955
 
930
956
  # Cleanup: kill any running threads
931
- idle_timer.cancel
957
+ shutting_down = true
958
+ idle_timer.shutdown
932
959
  current_task_thread&.kill
933
960
 
934
961
  # Save final session state
@@ -32,6 +32,7 @@ module Clacky
32
32
  @timer_thread = nil
33
33
  @compress_thread = nil
34
34
  @mutex = Mutex.new
35
+ @shutdown = false
35
36
  end
36
37
 
37
38
  # Start (or restart) the idle timer.
@@ -39,24 +40,35 @@ module Clacky
39
40
  def start
40
41
  cancel # reset any existing timer
41
42
 
42
- @timer_thread = Thread.new do
43
- Thread.current.name = "idle-compression-timer"
44
- sleep IDLE_DELAY
45
-
46
- # Register @compress_thread inside the mutex BEFORE the thread starts running,
47
- # so cancel() can always find and interrupt it even if it fires immediately.
48
- compress_thread = nil
49
- @mutex.synchronize do
50
- compress_thread = Thread.new do
51
- Thread.current.name = "idle-compression-work"
52
- run_compression
43
+ @mutex.synchronize do
44
+ return false if @shutdown
45
+
46
+ @timer_thread = Thread.new do
47
+ Thread.current.name = "idle-compression-timer"
48
+ sleep IDLE_DELAY
49
+ next if shutdown?
50
+
51
+ # Register @compress_thread inside the mutex BEFORE the thread starts running,
52
+ # so cancel() can always find and interrupt it even if it fires immediately.
53
+ compress_thread = nil
54
+ @mutex.synchronize do
55
+ unless @shutdown
56
+ compress_thread = Thread.new do
57
+ Thread.current.name = "idle-compression-work"
58
+ run_compression
59
+ end
60
+ @compress_thread = compress_thread
61
+ end
53
62
  end
54
- @compress_thread = compress_thread
55
- end
56
63
 
57
- compress_thread.join
58
- @mutex.synchronize { @compress_thread = nil; @timer_thread = nil }
64
+ compress_thread&.join
65
+ @mutex.synchronize { @compress_thread = nil; @timer_thread = nil }
66
+ end
59
67
  end
68
+ true
69
+ rescue ThreadError => e
70
+ log("Idle compression timer could not start: #{e.message}", level: :debug)
71
+ false
60
72
  end
61
73
 
62
74
  # Cancel the timer and any in-progress compression.
@@ -81,6 +93,13 @@ module Clacky
81
93
  compress_thread_to_join&.join(5)
82
94
  end
83
95
 
96
+ # Permanently stop this timer. Used during application shutdown so
97
+ # background agent-thread ensure blocks cannot create new timer threads.
98
+ def shutdown
99
+ @mutex.synchronize { @shutdown = true }
100
+ cancel
101
+ end
102
+
84
103
  # True if the timer or compression is currently active.
85
104
  def active?
86
105
  @mutex.synchronize { @timer_thread&.alive? || @compress_thread&.alive? }
@@ -94,6 +113,10 @@ module Clacky
94
113
  @mutex.synchronize { @compress_thread&.alive? || false }
95
114
  end
96
115
 
116
+ def shutdown?
117
+ @mutex.synchronize { @shutdown }
118
+ end
119
+
97
120
  private def run_compression
98
121
  success = @agent.trigger_idle_compression
99
122
 
@@ -170,8 +170,8 @@ module Clacky
170
170
  "name" => "Minimax",
171
171
  "base_url" => "https://api.minimaxi.com/v1",
172
172
  "api" => "openai-completions",
173
- "default_model" => "MiniMax-M2.7",
174
- "models" => ["MiniMax-M2.5", "MiniMax-M2.7"],
173
+ "default_model" => "MiniMax-M3",
174
+ "models" => ["MiniMax-M3", "MiniMax-M2.7", "MiniMax-M2.5"],
175
175
  # MiniMax operates two regional endpoints with identical APIs & model
176
176
  # lineup — mainland China (.com) and international (.io). Listing both
177
177
  # lets find_by_base_url identify either one as provider "minimax",
@@ -182,7 +182,12 @@ module Clacky
182
182
  { "label" => "International", "label_key" => "settings.models.baseurl.variant.international", "base_url" => "https://api.minimax.io/v1", "region" => "intl" }.freeze
183
183
  ].freeze,
184
184
  # MiniMax M2.x does not support multimodal/vision input on this endpoint.
185
+ # M3 (released 2026-06-01) is natively multimodal and accepts image
186
+ # input, so it overrides the provider-level vision=false below.
185
187
  "capabilities" => { "vision" => false }.freeze,
188
+ "model_capabilities" => {
189
+ "MiniMax-M3" => { "vision" => true }.freeze
190
+ }.freeze,
186
191
  "website_url" => "https://www.minimaxi.com/user-center/basic-information/interface-key"
187
192
  }.freeze,
188
193