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.
- checksums.yaml +4 -4
- data/.clacky/skills/commit/SKILL.md +11 -64
- data/CHANGELOG.md +25 -0
- data/lib/clacky/agent/cost_tracker.rb +11 -9
- data/lib/clacky/agent/llm_caller.rb +5 -2
- data/lib/clacky/agent/message_compressor.rb +1 -1
- data/lib/clacky/agent/session_serializer.rb +6 -20
- data/lib/clacky/agent.rb +29 -8
- data/lib/clacky/cli.rb +25 -64
- data/lib/clacky/default_skills/onboard/SKILL.md +16 -11
- data/lib/clacky/default_skills/skill-creator/SKILL.md +1 -1
- data/lib/clacky/idle_compression_timer.rb +92 -0
- data/lib/clacky/server/channel/adapters/feishu/adapter.rb +58 -0
- data/lib/clacky/server/channel/adapters/feishu/bot.rb +29 -0
- data/lib/clacky/server/channel/adapters/feishu/file_processor.rb +29 -0
- data/lib/clacky/server/channel/adapters/feishu/message_parser.rb +20 -9
- data/lib/clacky/server/channel/adapters/wecom/adapter.rb +40 -5
- data/lib/clacky/server/channel/adapters/wecom/media_downloader.rb +115 -0
- data/lib/clacky/server/channel/channel_manager.rb +8 -3
- data/lib/clacky/server/http_server.rb +101 -52
- data/lib/clacky/server/session_registry.rb +45 -32
- data/lib/clacky/session_manager.rb +18 -0
- data/lib/clacky/tools/shell.rb +34 -7
- data/lib/clacky/utils/file_attachment.rb +105 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +206 -30
- data/lib/clacky/web/app.js +59 -28
- data/lib/clacky/web/i18n.js +17 -15
- data/lib/clacky/web/index.html +28 -4
- data/lib/clacky/web/marked.min.js +69 -0
- data/lib/clacky/web/onboard.js +20 -9
- data/lib/clacky/web/sessions.js +275 -37
- data/lib/clacky/web/settings.js +10 -10
- data/lib/clacky/web/skills.js +1 -8
- data/lib/clacky/web/tasks.js +1 -7
- data/lib/clacky.rb +2 -0
- data/scripts/install.sh +7 -5
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9fbd7535e2cc98aadf0d9bbc3ff8dcc3db04669ba5c972ec00b41c338f1c3ae5
|
|
4
|
+
data.tar.gz: 345be84b2b010db47ccdd0454b0214463117519fe5d029440290c0187006ff4a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
210
|
+
### 6. Execute Commits Immediately
|
|
211
211
|
|
|
212
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
279
|
-
-
|
|
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
|
-
|
|
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
|
-
-
|
|
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
|
-
#
|
|
41
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
48
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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)}) - #{
|
|
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
|
-
|
|
417
|
-
say " #{idx + 1}. [#{session_id}] #{created_at} - #{
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
41
|
+
### 2. Ask the user to name the AI (card)
|
|
42
42
|
|
|
43
|
-
Call `request_user_feedback` to
|
|
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": "
|
|
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
|
-
|
|
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
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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…) —
|
|
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
|