openclacky 0.8.8 → 0.8.9

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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/.clacky/skills/commit/SKILL.md +11 -64
  3. data/CHANGELOG.md +25 -0
  4. data/lib/clacky/agent/cost_tracker.rb +11 -9
  5. data/lib/clacky/agent/llm_caller.rb +5 -2
  6. data/lib/clacky/agent/message_compressor.rb +1 -1
  7. data/lib/clacky/agent/session_serializer.rb +6 -20
  8. data/lib/clacky/agent.rb +29 -8
  9. data/lib/clacky/cli.rb +25 -64
  10. data/lib/clacky/default_skills/onboard/SKILL.md +16 -11
  11. data/lib/clacky/default_skills/skill-creator/SKILL.md +1 -1
  12. data/lib/clacky/idle_compression_timer.rb +92 -0
  13. data/lib/clacky/server/channel/adapters/feishu/adapter.rb +58 -0
  14. data/lib/clacky/server/channel/adapters/feishu/bot.rb +29 -0
  15. data/lib/clacky/server/channel/adapters/feishu/file_processor.rb +29 -0
  16. data/lib/clacky/server/channel/adapters/feishu/message_parser.rb +20 -9
  17. data/lib/clacky/server/channel/adapters/wecom/adapter.rb +40 -5
  18. data/lib/clacky/server/channel/adapters/wecom/media_downloader.rb +115 -0
  19. data/lib/clacky/server/channel/channel_manager.rb +8 -3
  20. data/lib/clacky/server/http_server.rb +101 -52
  21. data/lib/clacky/server/session_registry.rb +45 -32
  22. data/lib/clacky/session_manager.rb +18 -0
  23. data/lib/clacky/tools/shell.rb +34 -7
  24. data/lib/clacky/utils/file_attachment.rb +105 -0
  25. data/lib/clacky/version.rb +1 -1
  26. data/lib/clacky/web/app.css +206 -30
  27. data/lib/clacky/web/app.js +59 -28
  28. data/lib/clacky/web/i18n.js +17 -15
  29. data/lib/clacky/web/index.html +28 -4
  30. data/lib/clacky/web/marked.min.js +69 -0
  31. data/lib/clacky/web/onboard.js +20 -9
  32. data/lib/clacky/web/sessions.js +275 -37
  33. data/lib/clacky/web/settings.js +10 -10
  34. data/lib/clacky/web/skills.js +1 -8
  35. data/lib/clacky/web/tasks.js +1 -7
  36. data/lib/clacky.rb +2 -0
  37. data/scripts/install.sh +7 -5
  38. metadata +6 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 94fe2f9fd0477231e7411c762aacfecc9eb15b87f027366d7598c73e738499c0
4
- data.tar.gz: 41e84ac897d4d120dec9c7d88f69b35f82779adc2963a13dd099fd686d926ec3
3
+ metadata.gz: 9fbd7535e2cc98aadf0d9bbc3ff8dcc3db04669ba5c972ec00b41c338f1c3ae5
4
+ data.tar.gz: 345be84b2b010db47ccdd0454b0214463117519fe5d029440290c0187006ff4a
5
5
  SHA512:
6
- metadata.gz: 1f28d84aab760ca2c53cc0c558ec82fb3b0bebc8b2fe5279021f3d0cc5c896ebe5dda7fc8f060f7d77609bb6b6aedffa064ef1897185db01a9b6f4e022364646
7
- data.tar.gz: 64b71cd3c7793201f16600b100088cd739671c9c414252dadc6a593600f15271fc79a953877e59bc2014af929888384e3640ee78e7c1227c8b72e81f919ca72c
6
+ metadata.gz: 317f05f87355eb3c58da51dde828b6fafd3abbc7cc8bfa874c19445b5a783f7687ee84c7e8ddcf59573b7a554687ee8b2c9e5a8ed5d14993b6167fe5a656a881
7
+ data.tar.gz: ce669491b51cbff8f3fbdea2262d51d206377617d69ad981ac5025b824b86638d78d248ea6dcd89f4b9ecca92d042efa91b783dff220bec5c4ac348c8535b6d6
@@ -207,52 +207,11 @@ Based on the holistic analysis, generate commit messages following the conventio
207
207
  - `refactor: simplify database connection logic` (not one commit per file)
208
208
  - `docs: update API documentation` (only if pure documentation change)
209
209
 
210
- ### 6. Present Suggestions
210
+ ### 6. Execute Commits Immediately
211
211
 
212
- Show the user:
213
- - **The overall purpose/goal you identified**
214
- - List of proposed commits (prefer fewer, consolidated commits)
215
- - Files included in each commit
216
- - Commit message for each group
217
- - **Brief explanation of WHY changes were grouped this way**
212
+ No confirmation needed — analyze, group, and commit right away.
218
213
 
219
- Format:
220
- ```
221
- Overall goal: Implementing user authentication system
222
-
223
- Proposed commits:
224
-
225
- Commit 1: feat: add user authentication
226
- - lib/api/auth.rb (authentication logic)
227
- - lib/user.rb (user model updates)
228
- - lib/session.rb (session management)
229
- - spec/api/auth_spec.rb (tests)
230
- - spec/user_spec.rb (updated user tests)
231
- - config/routes.rb (auth routes)
232
-
233
- Reason: All these files work together to implement the authentication
234
- feature. Tests and configuration belong with the implementation.
235
-
236
- Commit 2: fix: resolve database timeout issue
237
- - lib/database/connection.rb
238
- - spec/database/connection_spec.rb
239
-
240
- Reason: Separate bug fix unrelated to authentication.
241
-
242
- Total: 2 commits (not 6+ small commits)
243
- ```
244
-
245
- ### 7. Get User Confirmation
246
-
247
- Ask the user:
248
- - Review the proposed commits
249
- - Confirm if they want to proceed
250
- - Allow modifications if needed
251
- - Get explicit approval before committing
252
-
253
- ### 8. Execute Commits
254
-
255
- For each approved commit:
214
+ For each commit group:
256
215
  ```bash
257
216
  # Stage specific files
258
217
  git add <file1> <file2> ...
@@ -261,22 +220,17 @@ git add <file1> <file2> ...
261
220
  git commit -m "<type>: <description>"
262
221
  ```
263
222
 
264
- **IMPORTANT**:
223
+ **IMPORTANT**:
265
224
  - Use ONLY `git commit -m "single line message"` format
266
225
  - DO NOT use multi-line commits with additional body text
267
226
  - DO NOT use `-m` flag multiple times
268
227
  - Keep the commit message as a single, concise line
269
228
 
270
- Provide feedback after each commit:
271
- - Confirm successful commit
272
- - Show commit hash
273
- - Display summary
274
-
275
- ### 9. Final Summary
229
+ ### 7. Final Summary
276
230
 
277
- After all commits:
278
- - Show total number of commits created
279
- - List all commit messages
231
+ After all commits, show:
232
+ - Total number of commits created
233
+ - Each commit hash + message
280
234
  - Suggest next steps (e.g., git push)
281
235
 
282
236
  ## Commands Used
@@ -370,11 +324,7 @@ AI (CORRECT APPROACH):
370
324
 
371
325
  Total: 3 meaningful commits instead of 5 fragmented ones
372
326
 
373
- Do you want to proceed? (yes/no)
374
-
375
- User: yes
376
-
377
- AI:
327
+ AI (executes immediately, no confirmation):
378
328
  Commit 1 created (a1b2c3d): feat: add user registration
379
329
  Commit 2 created (e4f5g6h): fix: correct password validation logic
380
330
  Commit 3 created (i7j8k9l): chore: update gem dependencies
@@ -438,11 +388,8 @@ For each set of changes, ask:
438
388
 
439
389
  ## Safety Features
440
390
 
441
- - Always review changes before committing
442
- - Require user confirmation before executing commits
443
- - Show exactly which files will be in each commit
444
- - Allow user to modify suggestions
445
- - Never force commits without approval
391
+ - Always review changes before committing (read diffs first)
392
+ - Execute commits immediately after analysis — no confirmation step
446
393
  - Preserve git history integrity
447
394
 
448
395
  ## Integration with Workflow
data/CHANGELOG.md CHANGED
@@ -7,6 +7,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.8.9] - 2026-03-13
11
+
12
+ ### Added
13
+ - **Markdown rendering in WebUI chat**: assistant responses are now rendered as rich markdown — headings, bold, code blocks, lists, and inline code are all formatted properly instead of displayed as raw text
14
+ - **Session naming with auto-name and inline rename**: sessions are automatically named after the first exchange; users can double-click any session in the sidebar to rename it inline
15
+ - **Session info bar with live status animation**: a slim bar below the chat header shows the session name, working directory, and a pulsing animation while the agent is thinking or executing tools
16
+ - **Restore last 5 sessions on startup**: the WebUI now reopens the five most recent sessions on startup instead of just the last one
17
+ - **Image and file support for Feishu and WeCom**: users can now send images and file attachments through Feishu and WeCom IM channels; the agent reads and processes them like any other input
18
+ - **Idle compression in WebUI**: the agent now compresses long conversation history automatically when the session has been idle, keeping context efficient without manual intervention
19
+
20
+ ### Improved
21
+ - **Onboard flow**: soul setup is now non-blocking; the confirmation page is skipped for a faster first-run experience; onboard now asks the user to name the AI first, then collects the user profile
22
+ - **Token usage display ordering**: the token usage line in WebUI now always appears below the assistant message bubble, not above it
23
+
24
+ ### Fixed
25
+ - **Token usage line disappears after page refresh**: token usage data is now persisted in session history and correctly re-rendered when the page is reloaded
26
+ - **Shell tool hangs on background commands**: commands containing `&` (background operator) no longer cause the shell tool to block indefinitely
27
+ - **White flash on page load**: the page is now hidden until boot completes, preventing a flash of unstyled content or the wrong view on startup
28
+ - **Theme flash on refresh**: the theme (dark/light) is now initialized inline in `<head>` so the correct colours are applied before any content renders
29
+ - **Onboard flash on reload**: the onboard panel no longer briefly appears when a session already exists during soul setup
30
+
31
+ ### More
32
+ - Rename channels "Test" button to "Diagnostics" for clarity
33
+ - Default-highlight the first item in skill autocomplete
34
+
10
35
  ## [0.8.8] - 2026-03-13
11
36
 
12
37
  ### Added
@@ -37,8 +37,8 @@ module Clacky
37
37
  end
38
38
  end
39
39
 
40
- # Display token usage statistics for this iteration
41
- display_iteration_tokens(usage, iteration_cost)
40
+ # Collect token usage data for this iteration (returned to caller for deferred display)
41
+ token_data = collect_iteration_tokens(usage, iteration_cost)
42
42
 
43
43
  # Update session bar cost in real-time (don't wait for agent.run to finish)
44
44
  @ui&.update_sessionbar(cost: @total_cost)
@@ -75,6 +75,9 @@ module Clacky
75
75
  @task_cache_stats[:cache_hit_requests] += 1
76
76
  end
77
77
  end
78
+
79
+ # Return token_data so the caller can display it at the right moment
80
+ token_data
78
81
  end
79
82
 
80
83
  # Estimate token count for a message content
@@ -147,10 +150,13 @@ module Clacky
147
150
 
148
151
  private
149
152
 
150
- # Display token usage for current iteration
153
+ # Collect token usage data for current iteration and return it.
154
+ # Does NOT call @ui directly — the caller is responsible for displaying
155
+ # at the right moment (e.g. after show_assistant_message).
151
156
  # @param usage [Hash] Usage data from API
152
157
  # @param cost [Float] Cost for this iteration
153
- def display_iteration_tokens(usage, cost)
158
+ # @return [Hash] token_data ready for show_token_usage
159
+ def collect_iteration_tokens(usage, cost)
154
160
  prompt_tokens = usage[:prompt_tokens] || 0
155
161
  completion_tokens = usage[:completion_tokens] || 0
156
162
  total_tokens = usage[:total_tokens] || (prompt_tokens + completion_tokens)
@@ -161,8 +167,7 @@ module Clacky
161
167
  delta_tokens = total_tokens - @previous_total_tokens
162
168
  @previous_total_tokens = total_tokens # Update for next iteration
163
169
 
164
- # Prepare data for UI to format and display
165
- token_data = {
170
+ {
166
171
  delta_tokens: delta_tokens,
167
172
  prompt_tokens: prompt_tokens,
168
173
  completion_tokens: completion_tokens,
@@ -171,9 +176,6 @@ module Clacky
171
176
  cache_read: cache_read,
172
177
  cost: cost
173
178
  }
174
-
175
- # Let UI handle formatting and display
176
- @ui&.show_token_usage(token_data)
177
179
  end
178
180
  end
179
181
  end
@@ -44,8 +44,11 @@ module Clacky
44
44
  @ui&.clear_progress
45
45
  end
46
46
 
47
- # Track cost for all LLM calls
48
- track_cost(response[:usage], raw_api_usage: response[:raw_api_usage])
47
+ # Track cost and collect token usage data.
48
+ # token_data is returned to the caller so it can be displayed
49
+ # after show_assistant_message (ensuring correct ordering in WebUI).
50
+ token_data = track_cost(response[:usage], raw_api_usage: response[:raw_api_usage])
51
+ response[:token_usage] = token_data
49
52
 
50
53
  response
51
54
  end
@@ -108,7 +108,7 @@ module Clacky
108
108
  def parse_compressed_result(result, chunk_path: nil)
109
109
  # Return the compressed result as a single assistant message
110
110
  # Keep the <analysis> or <summary> tags as they provide semantic context
111
- content = result.strip
111
+ content = result.to_s.strip
112
112
 
113
113
  if content.empty?
114
114
  []
@@ -9,6 +9,7 @@ module Clacky
9
9
  # @param session_data [Hash] Saved session data
10
10
  def restore_session(session_data)
11
11
  @session_id = session_data[:session_id]
12
+ @name = session_data[:name] || ""
12
13
  @messages = session_data[:messages]
13
14
  @todos = session_data[:todos] || [] # Restore todos from session
14
15
  @iterations = session_data.dig(:stats, :total_iterations) || 0
@@ -64,24 +65,6 @@ module Clacky
64
65
  # @param error_message [String] Error message if status is :error
65
66
  # @return [Hash] Session data ready for serialization
66
67
  def to_session_data(status: :success, error_message: nil)
67
- # Get last real user message for preview (skip compressed system messages)
68
- last_user_msg = @messages.reverse.find do |m|
69
- m[:role] == "user" && !m[:content].to_s.start_with?("[SYSTEM]")
70
- end
71
-
72
- # Extract preview text from last user message
73
- last_message_preview = if last_user_msg
74
- content = last_user_msg[:content]
75
- if content.is_a?(String)
76
- # Truncate to 100 characters for preview
77
- content.length > 100 ? "#{content[0..100]}..." : content
78
- else
79
- "User message (non-string content)"
80
- end
81
- else
82
- "No messages"
83
- end
84
-
85
68
  stats_data = {
86
69
  total_tasks: @total_tasks,
87
70
  total_iterations: @iterations,
@@ -98,6 +81,7 @@ module Clacky
98
81
 
99
82
  {
100
83
  session_id: @session_id,
84
+ name: @name,
101
85
  created_at: @created_at,
102
86
  updated_at: Time.now.iso8601,
103
87
  working_dir: @working_dir,
@@ -116,8 +100,7 @@ module Clacky
116
100
  verbose: @config.verbose
117
101
  },
118
102
  stats: stats_data,
119
- messages: @messages,
120
- last_user_message: last_message_preview
103
+ messages: @messages
121
104
  }
122
105
  end
123
106
 
@@ -211,6 +194,9 @@ module Clacky
211
194
  ui.show_tool_call(name, args)
212
195
  end
213
196
 
197
+ # Emit token usage stored on this message (for history replay display)
198
+ ui.show_token_usage(ev[:token_usage]) if ev[:token_usage]
199
+
214
200
  when "user"
215
201
  # Anthropic-format tool results (role: user, content: array of tool_result blocks)
216
202
  next unless ev[:content].is_a?(Array)
data/lib/clacky/agent.rb CHANGED
@@ -32,16 +32,20 @@ module Clacky
32
32
  include TimeMachine
33
33
  include MemoryUpdater
34
34
 
35
- attr_reader :session_id, :messages, :iterations, :total_cost, :working_dir, :created_at, :total_tasks, :todos,
36
- :cache_stats, :cost_source, :ui, :skill_loader, :agent_profile
35
+ attr_reader :session_id, :name, :messages, :iterations, :total_cost, :working_dir, :created_at, :total_tasks, :todos,
36
+ :cache_stats, :cost_source, :ui, :skill_loader, :agent_profile,
37
+ :status, :error, :updated_at
37
38
 
38
- def initialize(client, config , working_dir: , ui: , profile: )
39
+ def permission_mode = @config&.permission_mode&.to_s || ""
40
+
41
+ def initialize(client, config, working_dir:, ui:, profile:, session_id:)
39
42
  @client = client # Client for current model
40
43
  @config = config.is_a?(AgentConfig) ? config : AgentConfig.new(config)
41
44
  @agent_profile = AgentProfile.load(profile)
42
45
  @tool_registry = ToolRegistry.new
43
46
  @hooks = HookManager.new
44
- @session_id = SecureRandom.uuid
47
+ @session_id = session_id
48
+ @name = ""
45
49
  @messages = []
46
50
  @todos = [] # Store todos in memory
47
51
  @iterations = 0
@@ -92,7 +96,8 @@ module Clacky
92
96
  # Restore from a saved session
93
97
  def self.from_session(client, config, session_data, ui: nil, profile:)
94
98
  working_dir = session_data[:working_dir] || session_data["working_dir"] || Dir.pwd
95
- agent = new(client, config, working_dir: working_dir, ui: ui, profile: profile)
99
+ original_id = session_data[:session_id] || session_data["session_id"] || Clacky::SessionManager.generate_id
100
+ agent = new(client, config, working_dir: working_dir, ui: ui, profile: profile, session_id: original_id)
96
101
  agent.restore_session(session_data)
97
102
  agent
98
103
  end
@@ -141,6 +146,11 @@ module Clacky
141
146
  @config.model_name
142
147
  end
143
148
 
149
+ # Rename this session. Called by auto-naming (first message) or user explicit rename.
150
+ def rename(new_name)
151
+ @name = new_name.to_s.strip
152
+ end
153
+
144
154
  def run(user_input, images: [], files: [])
145
155
  # Start new task for Time Machine
146
156
  task_id = start_new_task
@@ -210,11 +220,14 @@ module Clacky
210
220
  if response[:finish_reason] == "stop" || response[:tool_calls].nil? || response[:tool_calls].empty?
211
221
  # During memory update phase, show LLM response as info (not a chat bubble)
212
222
  if @memory_updating && response[:content] && !response[:content].empty?
213
- @ui&.show_info("🧠 " + response[:content].strip)
223
+ @ui&.show_info(response[:content].strip)
214
224
  elsif response[:content] && !response[:content].empty?
215
225
  @ui&.show_assistant_message(response[:content])
216
226
  end
217
227
 
228
+ # Show token usage after the assistant message so WebUI renders it below the bubble
229
+ @ui&.show_token_usage(response[:token_usage]) if response[:token_usage]
230
+
218
231
  # Debug: log why we're stopping
219
232
  if @config.verbose && (response[:tool_calls].nil? || response[:tool_calls].empty?)
220
233
  reason = response[:finish_reason] == "stop" ? "API returned finish_reason=stop" : "No tool calls in response"
@@ -237,6 +250,10 @@ module Clacky
237
250
  @ui&.show_assistant_message(response[:content])
238
251
  end
239
252
 
253
+ # Show token usage after assistant message (or immediately if no message).
254
+ # This ensures WebUI renders the token line below the assistant bubble.
255
+ @ui&.show_token_usage(response[:token_usage]) if response[:token_usage]
256
+
240
257
  # Act: Execute tool calls
241
258
  action_result = act(response[:tool_calls])
242
259
 
@@ -404,6 +421,8 @@ module Clacky
404
421
  if response[:tool_calls]&.any?
405
422
  msg[:tool_calls] = format_tool_calls_for_api(response[:tool_calls])
406
423
  end
424
+ # Store token_usage in the message so replay_history can re-emit it
425
+ msg[:token_usage] = response[:token_usage] if response[:token_usage]
407
426
  @messages << msg
408
427
 
409
428
  response
@@ -668,7 +687,7 @@ module Clacky
668
687
  @tool_registry.register(Tools::WebSearch.new)
669
688
  @tool_registry.register(Tools::WebFetch.new)
670
689
  @tool_registry.register(Tools::TodoManager.new)
671
- @tool_registry.register(Tools::RunProject.new)
690
+ # @tool_registry.register(Tools::RunProject.new) # temporarily disabled
672
691
  @tool_registry.register(Tools::RequestUserFeedback.new)
673
692
  @tool_registry.register(Tools::InvokeSkill.new)
674
693
  @tool_registry.register(Tools::UndoTask.new)
@@ -717,12 +736,14 @@ module Clacky
717
736
  )
718
737
 
719
738
  # Create subagent (reuses all tools from parent, inherits agent profile from parent)
739
+ # Subagent gets its own unique session_id.
720
740
  subagent = self.class.new(
721
741
  subagent_client,
722
742
  subagent_config,
723
743
  working_dir: @working_dir,
724
744
  ui: @ui,
725
- profile: @agent_profile.name
745
+ profile: @agent_profile.name,
746
+ session_id: Clacky::SessionManager.generate_id
726
747
  )
727
748
  subagent.instance_variable_set(:@is_subagent, true)
728
749
 
data/lib/clacky/cli.rb CHANGED
@@ -102,7 +102,8 @@ module Clacky
102
102
  end
103
103
 
104
104
  # Create new agent if no session loaded
105
- agent ||= Clacky::Agent.new(client, agent_config, working_dir: working_dir, ui: nil, profile: agent_profile)
105
+ agent ||= Clacky::Agent.new(client, agent_config, working_dir: working_dir, ui: nil, profile: agent_profile,
106
+ session_id: Clacky::SessionManager.generate_id)
106
107
 
107
108
  # Change to working directory
108
109
  original_dir = Dir.pwd
@@ -358,11 +359,11 @@ module Clacky
358
359
  session_id = session[:session_id][0..7]
359
360
  tasks = session.dig(:stats, :total_tasks) || 0
360
361
  cost = session.dig(:stats, :total_cost_usd) || 0.0
361
- last_msg = session[:last_user_message] || "No message"
362
+ name = session[:name].to_s.empty? ? "Unnamed session" : session[:name]
362
363
  is_current_dir = session[:working_dir] == working_dir
363
364
 
364
365
  dir_marker = is_current_dir ? "📍" : " "
365
- say "#{dir_marker} #{index + 1}. [#{session_id}] #{created_at} (#{tasks} tasks, $#{cost.round(4)}) - #{last_msg}", :cyan
366
+ say "#{dir_marker} #{index + 1}. [#{session_id}] #{created_at} (#{tasks} tasks, $#{cost.round(4)}) - #{name}", :cyan
366
367
  end
367
368
  say "\n\n💡 Use `clacky -a <session_id>` to resume a session.", :yellow
368
369
  say ""
@@ -413,8 +414,8 @@ module Clacky
413
414
  matching_sessions.each_with_index do |session, idx|
414
415
  created_at = Time.parse(session[:created_at]).strftime("%Y-%m-%d %H:%M")
415
416
  session_id = session[:session_id][0..7]
416
- last_msg = session[:last_user_message] || "No message"
417
- say " #{idx + 1}. [#{session_id}] #{created_at} - #{last_msg}", :cyan
417
+ name = session[:name].to_s.empty? ? "Unnamed session" : session[:name]
418
+ say " #{idx + 1}. [#{session_id}] #{created_at} - #{name}", :cyan
418
419
  end
419
420
  say "\nPlease use a more specific prefix.", :yellow
420
421
  exit 1
@@ -514,7 +515,8 @@ module Clacky
514
515
  when "/exit", "/quit"
515
516
  break
516
517
  when "/clear"
517
- agent = Clacky::Agent.new(client, agent_config, working_dir: working_dir, ui: nil, profile: profile)
518
+ agent = Clacky::Agent.new(client, agent_config, working_dir: working_dir, ui: nil, profile: profile,
519
+ session_id: Clacky::SessionManager.generate_id)
518
520
  agent.instance_variable_set(:@ui, json_ui)
519
521
  json_ui.emit("info", message: "Session cleared. Starting fresh.")
520
522
  next
@@ -584,9 +586,19 @@ module Clacky
584
586
  ui_controller.set_skill_loader(agent.skill_loader, agent.agent_profile)
585
587
 
586
588
  # Track current working thread (agent or idle compression that can be interrupted)
587
- # idle_timer is tracked separately because it should not be interrupted during sleep
588
589
  current_task_thread = nil
589
- idle_timer_thread = nil
590
+
591
+ # Idle compression timer - triggers compression after 180s of inactivity
592
+ idle_timer = Clacky::IdleCompressionTimer.new(
593
+ agent: agent,
594
+ session_manager: session_manager,
595
+ logger: ->(msg, level:) { ui_controller.log(msg, level: level) }
596
+ ) do |success|
597
+ if success
598
+ ui_controller.update_sessionbar(tasks: agent.total_tasks, cost: agent.total_cost)
599
+ end
600
+ ui_controller.set_idle_status
601
+ end
590
602
 
591
603
  # Set up mode toggle handler
592
604
  ui_controller.on_mode_toggle do |new_mode|
@@ -647,7 +659,7 @@ module Clacky
647
659
  # Clear output area
648
660
  ui_controller.layout.clear_output
649
661
  # Clear session by creating a new agent
650
- agent = Clacky::Agent.new(client, agent_config, working_dir: working_dir, ui: ui_controller, profile: agent.agent_profile.name)
662
+ agent = Clacky::Agent.new(client, agent_config, working_dir: working_dir, ui: ui_controller, profile: agent.agent_profile.name, session_id: Clacky::SessionManager.generate_id)
651
663
  ui_controller.show_info("Session cleared. Starting fresh.")
652
664
  # Update session bar with reset values
653
665
  ui_controller.update_sessionbar(tasks: agent.total_tasks, cost: agent.total_cost)
@@ -671,59 +683,7 @@ module Clacky
671
683
  end
672
684
 
673
685
  # Cancel idle timer if running (new input means user is active)
674
- if idle_timer_thread&.alive?
675
- # ui_controller.log("Idle timer killed, start new 1", level: :debug)
676
- idle_timer_thread.kill
677
- idle_timer_thread = nil
678
- end
679
-
680
- # Helper method to start idle timer after agent completes
681
- start_idle_timer = lambda do
682
- # Cancel any existing idle timer first
683
- if idle_timer_thread&.alive?
684
- # ui_controller.log("Idle timer killed, start new 2", level: :debug)
685
- idle_timer_thread.kill
686
- idle_timer_thread = nil
687
- end
688
-
689
- # Start idle timer - trigger compression after 180 seconds of inactivity
690
- idle_timer_thread = Thread.new do
691
- # ui_controller.log("Idle timer started, will trigger compression in 180 seconds", level: :debug)
692
- # Sleep outside of rescue block - if interrupted here, let it propagate and exit
693
- sleep 180
694
- # ui_controller.log("Idle timer sleep completed, starting compression", level: :debug)
695
-
696
- # After sleep completes, switch to current_task_thread for compression
697
- # (so it can be interrupted by Ctrl+C)
698
- current_task_thread = Thread.new do
699
- begin
700
- # After 60 seconds, start idle compression
701
- ui_controller.set_working_status
702
- success = agent.trigger_idle_compression
703
-
704
- if success
705
- # Update session bar after compression
706
- ui_controller.update_sessionbar(tasks: agent.total_tasks, cost: agent.total_cost)
707
- # Save session after compression
708
- session_manager&.save(agent.to_session_data(status: :success))
709
- end
710
- rescue Clacky::AgentInterrupted
711
- # Compression was interrupted by user
712
- ui_controller.append_output("")
713
- ui_controller.show_info("Idle compression cancelled")
714
- rescue => e
715
- ui_controller.log("Idle compression error: #{e.message}", level: :error)
716
- ensure
717
- ui_controller.set_idle_status
718
- current_task_thread = nil
719
- end
720
- end
721
-
722
- # Wait for compression to complete
723
- current_task_thread.join
724
- idle_timer_thread = nil
725
- end
726
- end
686
+ idle_timer.cancel
727
687
 
728
688
  # Run agent in background thread
729
689
  current_task_thread = Thread.new do
@@ -747,7 +707,7 @@ module Clacky
747
707
  ensure
748
708
  current_task_thread = nil
749
709
  # Start idle timer after agent completes
750
- start_idle_timer.call
710
+ idle_timer.start
751
711
  end
752
712
  end
753
713
  end
@@ -765,7 +725,8 @@ module Clacky
765
725
  # Start input loop (blocks until exit)
766
726
  ui_controller.start_input_loop
767
727
 
768
- # Cleanup: kill any running thread
728
+ # Cleanup: kill any running threads
729
+ idle_timer.cancel
769
730
  current_task_thread&.kill
770
731
 
771
732
  # Save final session state
@@ -38,35 +38,39 @@ Example (Chinese):
38
38
  > 嗨!我是你的专属 AI 助手 ⚡
39
39
  > 只需 30 秒完成个性化设置,我会问你两个简单问题。
40
40
 
41
- ### 2. Ask the user's name (card)
41
+ ### 2. Ask the user to name the AI (card)
42
42
 
43
- Call `request_user_feedback` to get the user's preferred name.
43
+ Call `request_user_feedback` to let the user pick or type a name for their AI assistant.
44
+ Offer a few fun suggestions as options, plus a free-text fallback.
44
45
 
45
46
  If `lang == "zh"`, use:
46
47
  ```json
47
48
  {
48
- "question": "先告诉我,我该怎么称呼你?"
49
+ "question": "先来点有意思的 —— 你想叫我什么名字?可以选一个,也可以直接输入你喜欢的:",
50
+ "options": ["🐟 摸鱼王", "📚 卷王", "🌟 小天才", "🐱 本喵", "🌅 拾光", "自己输入名字…"]
49
51
  }
50
52
  ```
51
53
 
52
54
  Otherwise (English):
53
55
  ```json
54
56
  {
55
- "question": "First, what should I call you?"
57
+ "question": "Let's start with something fun — what would you like to call me? Pick one or type your own:",
58
+ "options": ["✨ Aria", "🤖 Max", "🌙 Luna", "⚡ Zap", "🎯 Ace", "Type your own name…"]
56
59
  }
57
60
  ```
58
61
 
59
- Store the reply as `user.name` (default `"there"` for English, `"朋友"` for Chinese if blank).
62
+ If the user selects the last option or types a custom name, use that as-is. If they chose from the list, strip any emoji prefix.
63
+ Store the result as `ai.name` (default `"Clacky"` if blank).
60
64
 
61
65
  ### 3. Collect AI personality (card)
62
66
 
63
67
  Call `request_user_feedback` with a card to set the assistant's personality.
64
- Address the user by `user.name` in the question.
68
+ Address the AI by `ai.name` in the question.
65
69
 
66
70
  If `lang == "zh"`, use:
67
71
  ```json
68
72
  {
69
- "question": "好的,[user.name]!来设置一下你的助手吧。",
73
+ "question": "好的![ai.name] 应该是什么风格呢?",
70
74
  "options": [
71
75
  "🎯 专业型 — 精准、结构化、不废话",
72
76
  "😊 友好型 — 热情、鼓励、像一位博学的朋友",
@@ -79,7 +83,7 @@ If `lang == "zh"`, use:
79
83
  Otherwise (English):
80
84
  ```json
81
85
  {
82
- "question": "Nice to meet you, [user.name]! Now let's set up your assistant.",
86
+ "question": "Great! What personality should [ai.name] have?",
83
87
  "options": [
84
88
  "🎯 Professional — Precise, structured, minimal filler",
85
89
  "😊 Friendly — Warm, encouraging, like a knowledgeable friend",
@@ -99,12 +103,12 @@ Store: `ai.personality`.
99
103
 
100
104
  ### 4. Collect user profile (card)
101
105
 
102
- Call `request_user_feedback` again.
106
+ Call `request_user_feedback` again. This is where we learn about the user themselves.
103
107
 
104
108
  If `lang == "zh"`, use:
105
109
  ```json
106
110
  {
107
- "question": "再简单了解一下你自己吧 —— 全部可选,随便填:\n• 职业\n• 最希望用 AI 做什么\n• 社交 / 作品链接(GitHub、微博、个人网站等)—— AI 会读取公开信息来了解你",
111
+ "question": "那你呢?随便聊聊自己吧 —— 全部可选,填多少都行:\n• 你的名字(我该怎么称呼你?)\n• 职业\n• 最希望用 AI 做什么\n• 社交 / 作品链接(GitHub、微博、个人网站等)—— 我会读取公开信息来更了解你",
108
112
  "options": []
109
113
  }
110
114
  ```
@@ -112,12 +116,13 @@ If `lang == "zh"`, use:
112
116
  Otherwise (English):
113
117
  ```json
114
118
  {
115
- "question": "Now a bit about you — all optional, skip anything you like.\n• Occupation\n• What you want to use AI for most\n• Social / portfolio links (GitHub, Twitter/X, personal site…) — AI will read them to learn about you",
119
+ "question": "Now a bit about you — all optional, skip anything you like.\n• Your name (what should I call you?)\n• Occupation\n• What you want to use AI for most\n• Social / portfolio links (GitHub, Twitter/X, personal site…) — I'll read them to learn about you",
116
120
  "options": []
117
121
  }
118
122
  ```
119
123
 
120
124
  Parse the user's reply as free text; extract whatever they provide.
125
+ Store the user's name as `user.name` (default `"老大"` for Chinese, `"Boss"` for English if blank).
121
126
 
122
127
  ### 5. Learn from links (if any)
123
128
 
@@ -28,7 +28,7 @@ Always be flexible. If the user says "skip the evals, just vibe with me", do tha
28
28
 
29
29
  This skill runs inside **Clacky** (openclacky). Key platform specifics:
30
30
 
31
- - **Skills** live at `~/.clacky/skills/<skill-name>/` — **always create new skills here** (global user skills, visible to Web UI and all sessions)
31
+ - **Skills** live at `~/.clacky/skills/<skill-name>/` — **always create new skills here** (global user skills, visible to Web UI and all sessions). To locate an existing skill, check these paths in order using `glob` or `ls`: (1) `.clacky/skills/` — project-level skills, (2) `~/.clacky/skills/` — user-level skills. Built-in skills (shipped with the gem) are always available via `invoke_skill` by name — no file lookup needed. Never use `find /` or broad filesystem searches to locate skills.
32
32
  - **No parallel subagents** — Clacky runs as a single agent; all test cases execute serially in the current session
33
33
  - **No external agent CLI** — for evals, just execute the task directly in-session (read the skill, follow instructions, save outputs)
34
34
  - **Scripts** — prefer **Ruby** (`.rb` files); Clacky is Ruby-native. Run with `ruby path/to/script.rb`. Python is available but Ruby is the default choice