openclacky 0.9.29 → 0.9.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +17 -0
- data/lib/clacky/agent/llm_caller.rb +14 -2
- data/lib/clacky/agent/message_compressor.rb +24 -6
- data/lib/clacky/agent/message_compressor_helper.rb +16 -8
- data/lib/clacky/agent/session_serializer.rb +69 -0
- data/lib/clacky/agent/skill_manager.rb +2 -2
- data/lib/clacky/agent.rb +3 -0
- data/lib/clacky/brand_config.rb +25 -0
- data/lib/clacky/cli.rb +13 -0
- data/lib/clacky/client.rb +16 -3
- data/lib/clacky/json_ui_controller.rb +16 -10
- data/lib/clacky/message_history.rb +8 -0
- data/lib/clacky/plain_ui_controller.rb +1 -6
- data/lib/clacky/providers.rb +23 -4
- data/lib/clacky/server/channel/adapters/wecom/ws_client.rb +3 -1
- data/lib/clacky/server/channel/adapters/weixin/adapter.rb +5 -5
- data/lib/clacky/server/web_ui_controller.rb +17 -9
- data/lib/clacky/ui2/ui_controller.rb +1 -5
- data/lib/clacky/ui_interface.rb +5 -7
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +8 -0
- data/lib/clacky/web/app.js +28 -10
- data/lib/clacky/web/i18n.js +57 -0
- data/lib/clacky/web/sessions.js +60 -31
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 85678c695dcd96512aecef3a290beefa0f08b605a351da12c2db6072d6852147
|
|
4
|
+
data.tar.gz: 298820c8aabc0a13cb874fa577a5cf12757aa403b804c79d0fa804a5142ecbd0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1d291319e545f25c169341a14e54aa740d17107e2cd97ce39d9285ba7820ba69e3f10f14c15e26e579bfe71d471023be15a6e7ff225131fd77aeb9b916cbcc79
|
|
7
|
+
data.tar.gz: f89731bd9fc60ca68def237f4cbda3d998dfa306da9419e24b2c3f2dc6cf91c007a80e8bb658685a85d936dab58f246b55bbed7ead92637f0d2b857e645884af
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.9.30] - 2026-04-16
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **OpenClacky provider support**: new built-in provider preset for OpenClacky API (https://api.openclacky.com) with Claude Opus 4.6, Sonnet 4.6/4.5, and Haiku 4.5 models
|
|
14
|
+
- **Session chunk index system**: compressed conversation chunks now include a searchable index with topics and turn counts — the agent can selectively load only relevant historical context instead of re-reading all compressed messages, dramatically reducing token usage in long sessions
|
|
15
|
+
- **Provider availability indicator**: Web UI now shows a real-time status badge (Available/Unavailable) next to each provider in the settings modal, helping users quickly identify which services are reachable
|
|
16
|
+
|
|
17
|
+
### Improved
|
|
18
|
+
- **Progress streaming UX**: API call progress messages (e.g., "Agent is thinking...", compression updates) are now streamed incrementally to the Web UI with better visual feedback and reduced latency
|
|
19
|
+
- **Brand name localization**: brand skill metadata now includes configurable Chinese names (`name_zh`) for better display in localized UIs
|
|
20
|
+
- **Idle timer reliability**: fixed a race condition where old idle timers from previous CLI sessions could continue running after restarting, causing premature auto-saves
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
- **Prompt caching in subagents**: subagent tool calls (e.g., skills invoked via `invoke_skill`) now correctly inherit and propagate prompt caching behavior from the parent agent, reducing redundant API costs
|
|
24
|
+
- **WeChat Work Ruby 3.1 compatibility**: fixed `Queue.empty?` crash on Ruby < 3.2 in WeCom channel WebSocket client (method was added in Ruby 3.2.0)
|
|
25
|
+
- **WeChat markdown stripping**: incoming messages from WeChat (Weixin) now preserve original text content when stripping markdown decorators, fixing message corruption where text was accidentally removed
|
|
26
|
+
|
|
10
27
|
## [0.9.29] - 2026-04-15
|
|
11
28
|
|
|
12
29
|
### Added
|
|
@@ -81,10 +81,16 @@ module Clacky
|
|
|
81
81
|
# infrastructure blips — do NOT trigger fallback. Just retry on the current
|
|
82
82
|
# model (primary or already-active fallback) up to max_retries.
|
|
83
83
|
if retries <= max_retries
|
|
84
|
-
@ui&.
|
|
84
|
+
@ui&.show_progress(
|
|
85
|
+
"Network failed: #{e.message}",
|
|
86
|
+
progress_type: "retrying",
|
|
87
|
+
phase: "active",
|
|
88
|
+
metadata: { attempt: retries, total: max_retries }
|
|
89
|
+
)
|
|
85
90
|
sleep retry_delay
|
|
86
91
|
retry
|
|
87
92
|
else
|
|
93
|
+
@ui&.show_progress(progress_type: "retrying", phase: "done")
|
|
88
94
|
@ui&.show_error("Network failed after #{max_retries} retries: #{e.message}")
|
|
89
95
|
raise AgentError, "Network connection failed after #{max_retries} retries: #{e.message}"
|
|
90
96
|
end
|
|
@@ -112,10 +118,16 @@ module Clacky
|
|
|
112
118
|
retry
|
|
113
119
|
end
|
|
114
120
|
end
|
|
115
|
-
@ui&.
|
|
121
|
+
@ui&.show_progress(
|
|
122
|
+
e.message,
|
|
123
|
+
progress_type: "retrying",
|
|
124
|
+
phase: "active",
|
|
125
|
+
metadata: { attempt: retries, total: current_max }
|
|
126
|
+
)
|
|
116
127
|
sleep retry_delay
|
|
117
128
|
retry
|
|
118
129
|
else
|
|
130
|
+
@ui&.show_progress(progress_type: "retrying", phase: "done")
|
|
119
131
|
@ui&.show_error("LLM service unavailable after #{current_max} retries. Please try again later.")
|
|
120
132
|
raise AgentError, "LLM service unavailable after #{current_max} retries"
|
|
121
133
|
end
|
|
@@ -38,16 +38,23 @@ module Clacky
|
|
|
38
38
|
YOUR ONLY TASK: Create a comprehensive summary of the conversation above.
|
|
39
39
|
|
|
40
40
|
REQUIRED RESPONSE FORMAT:
|
|
41
|
-
|
|
41
|
+
First output a <topics> line listing 3-6 key topic phrases (comma-separated, concise).
|
|
42
|
+
Then output the full summary wrapped in <summary> tags.
|
|
42
43
|
|
|
43
|
-
|
|
44
|
+
Example format:
|
|
45
|
+
<topics>Rails setup, database config, deploy pipeline, Tailwind CSS</topics>
|
|
46
|
+
<summary>
|
|
47
|
+
...full summary text...
|
|
48
|
+
</summary>
|
|
49
|
+
|
|
50
|
+
Focus on:
|
|
44
51
|
- User's explicit requests and intents
|
|
45
52
|
- Key technical concepts and code changes
|
|
46
53
|
- Files examined and modified
|
|
47
54
|
- Errors encountered and fixes applied
|
|
48
55
|
- Current work status and pending tasks
|
|
49
56
|
|
|
50
|
-
Begin your
|
|
57
|
+
Begin your response NOW. Remember: PURE TEXT only, starting with <topics> then <summary>.
|
|
51
58
|
PROMPT
|
|
52
59
|
|
|
53
60
|
def initialize(client, model: nil)
|
|
@@ -109,22 +116,33 @@ module Clacky
|
|
|
109
116
|
end
|
|
110
117
|
|
|
111
118
|
|
|
119
|
+
# Parse topics tag from compressed content.
|
|
120
|
+
# Returns the topics string if found, nil otherwise.
|
|
121
|
+
# e.g. "<topics>Rails setup, database config</topics>" → "Rails setup, database config"
|
|
122
|
+
def parse_topics(content)
|
|
123
|
+
m = content.match(/<topics>(.*?)<\/topics>/m)
|
|
124
|
+
m ? m[1].strip : nil
|
|
125
|
+
end
|
|
126
|
+
|
|
112
127
|
def parse_compressed_result(result, chunk_path: nil)
|
|
113
128
|
# Return the compressed result as a single assistant message
|
|
114
|
-
# Keep the <
|
|
129
|
+
# Keep the <summary> tags as they provide semantic context
|
|
115
130
|
content = result.to_s.strip
|
|
116
131
|
|
|
117
132
|
if content.empty?
|
|
118
133
|
[]
|
|
119
134
|
else
|
|
135
|
+
# Strip out the <topics> block — it's metadata for the chunk file, not for AI context
|
|
136
|
+
content_without_topics = content.gsub(/<topics>.*?<\/topics>\n*/m, "").strip
|
|
137
|
+
|
|
120
138
|
# Inject chunk anchor so AI knows where to find original conversation
|
|
121
139
|
if chunk_path
|
|
122
140
|
anchor = "\n\n---\n📁 **Original conversation archived at:** `#{chunk_path}`\n" \
|
|
123
141
|
"_Use `file_reader` tool to recall details from this chunk._"
|
|
124
|
-
|
|
142
|
+
content_without_topics = content_without_topics + anchor
|
|
125
143
|
end
|
|
126
144
|
|
|
127
|
-
[{ role: "assistant", content:
|
|
145
|
+
[{ role: "assistant", content: content_without_topics, compressed_summary: true, chunk_path: chunk_path }]
|
|
128
146
|
end
|
|
129
147
|
end
|
|
130
148
|
end
|
|
@@ -17,9 +17,9 @@ module Clacky
|
|
|
17
17
|
def trigger_idle_compression
|
|
18
18
|
# Check if we should compress (force mode)
|
|
19
19
|
compression_context = compress_messages_if_needed(force: true)
|
|
20
|
-
@ui&.
|
|
20
|
+
@ui&.show_progress("Idle detected. Compressing conversation to optimize costs...", progress_type: "idle_compress", phase: "active")
|
|
21
21
|
if compression_context.nil?
|
|
22
|
-
@ui&.
|
|
22
|
+
@ui&.show_progress("Idle skipped.", progress_type: "idle_compress", phase: "done")
|
|
23
23
|
Clacky::Logger.info(
|
|
24
24
|
"Idle compression skipped",
|
|
25
25
|
enable_compression: @config.enable_compression,
|
|
@@ -137,7 +137,8 @@ module Clacky
|
|
|
137
137
|
original_messages,
|
|
138
138
|
compression_context[:recent_messages],
|
|
139
139
|
chunk_index: chunk_index,
|
|
140
|
-
compression_level: compression_context[:compression_level]
|
|
140
|
+
compression_level: compression_context[:compression_level],
|
|
141
|
+
topics: @message_compressor.parse_topics(compressed_content)
|
|
141
142
|
)
|
|
142
143
|
|
|
143
144
|
@history.replace_all(@message_compressor.rebuild_with_compression(
|
|
@@ -167,7 +168,7 @@ module Clacky
|
|
|
167
168
|
# Show compression info (use estimated tokens from rebuilt history)
|
|
168
169
|
compression_summary = "History compressed (~#{compression_context[:original_token_count]} -> ~#{@history.estimate_tokens} tokens, " \
|
|
169
170
|
"level #{compression_context[:compression_level]})"
|
|
170
|
-
@ui&.
|
|
171
|
+
@ui&.show_progress(compression_summary, progress_type: "idle_compress", phase: "done")
|
|
171
172
|
end
|
|
172
173
|
|
|
173
174
|
# Get recent messages while preserving tool_calls/tool_results pairs.
|
|
@@ -303,16 +304,21 @@ module Clacky
|
|
|
303
304
|
# @param recent_messages [Array<Hash>] Recent messages being kept (to exclude from chunk)
|
|
304
305
|
# @param chunk_index [Integer] Sequential chunk number
|
|
305
306
|
# @param compression_level [Integer] Compression level
|
|
307
|
+
# @param topics [String, nil] Short topic description for chunk index card
|
|
306
308
|
# @return [String, nil] Path to saved chunk file, or nil if save failed
|
|
307
|
-
def save_compressed_chunk(original_messages, recent_messages, chunk_index:, compression_level:)
|
|
309
|
+
def save_compressed_chunk(original_messages, recent_messages, chunk_index:, compression_level:, topics: nil)
|
|
308
310
|
return nil unless @session_id && @created_at
|
|
309
311
|
|
|
310
312
|
# Messages being compressed = original minus system message minus recent messages
|
|
311
313
|
# Also exclude system-injected scaffolding (session context, memory prompts, etc.)
|
|
312
314
|
# — these are internal CLI metadata and must not appear in chunk MD or WebUI history.
|
|
315
|
+
# Also exclude previous compressed_summary messages: they are index cards pointing
|
|
316
|
+
# to older chunk files and must NOT be embedded inside a new chunk, otherwise
|
|
317
|
+
# parse_chunk_md_to_rounds would follow the nested reference and create circular
|
|
318
|
+
# chunk chains (chunk-2 → chunk-1 → ... → chunk-2).
|
|
313
319
|
recent_set = recent_messages.to_a
|
|
314
320
|
messages_to_archive = original_messages.reject do |m|
|
|
315
|
-
m[:role] == "system" || m[:system_injected] || recent_set.include?(m)
|
|
321
|
+
m[:role] == "system" || m[:system_injected] || m[:compressed_summary] || recent_set.include?(m)
|
|
316
322
|
end
|
|
317
323
|
|
|
318
324
|
return nil if messages_to_archive.empty?
|
|
@@ -324,7 +330,7 @@ module Clacky
|
|
|
324
330
|
chunk_filename = "#{base_name}-chunk-#{chunk_index}.md"
|
|
325
331
|
chunk_path = File.join(sessions_dir, chunk_filename)
|
|
326
332
|
|
|
327
|
-
md_content = build_chunk_md(messages_to_archive, chunk_index: chunk_index, compression_level: compression_level)
|
|
333
|
+
md_content = build_chunk_md(messages_to_archive, chunk_index: chunk_index, compression_level: compression_level, topics: topics)
|
|
328
334
|
|
|
329
335
|
File.write(chunk_path, md_content)
|
|
330
336
|
FileUtils.chmod(0o600, chunk_path)
|
|
@@ -339,8 +345,9 @@ module Clacky
|
|
|
339
345
|
# @param messages [Array<Hash>] Messages to render
|
|
340
346
|
# @param chunk_index [Integer] Chunk number for metadata
|
|
341
347
|
# @param compression_level [Integer] Compression level
|
|
348
|
+
# @param topics [String, nil] Short topic description extracted from LLM summary
|
|
342
349
|
# @return [String] Markdown content
|
|
343
|
-
def build_chunk_md(messages, chunk_index:, compression_level:)
|
|
350
|
+
def build_chunk_md(messages, chunk_index:, compression_level:, topics: nil)
|
|
344
351
|
lines = []
|
|
345
352
|
|
|
346
353
|
# Front matter
|
|
@@ -350,6 +357,7 @@ module Clacky
|
|
|
350
357
|
lines << "compression_level: #{compression_level}"
|
|
351
358
|
lines << "archived_at: #{Time.now.iso8601}"
|
|
352
359
|
lines << "message_count: #{messages.size}"
|
|
360
|
+
lines << "topics: #{topics}" if topics
|
|
353
361
|
lines << "---"
|
|
354
362
|
lines << ""
|
|
355
363
|
lines << "# Session Chunk #{chunk_index}"
|
|
@@ -529,6 +529,75 @@ module Clacky
|
|
|
529
529
|
{ name: "image_#{idx + 1}.#{ext}", mime_type: mime_type, data_url: url, path: path }
|
|
530
530
|
end
|
|
531
531
|
end
|
|
532
|
+
|
|
533
|
+
# Inject a chunk index card into the conversation when archived chunks exist.
|
|
534
|
+
# Lists all chunk files (path + topics + turn count) so the AI knows where to
|
|
535
|
+
# look if it needs details from past conversations. The AI can load any chunk
|
|
536
|
+
# on demand using the existing file_reader tool — no new tools required.
|
|
537
|
+
#
|
|
538
|
+
# Only re-injects when a new chunk has been added since the last injection,
|
|
539
|
+
# keeping the message list clean across multiple compressions.
|
|
540
|
+
#
|
|
541
|
+
# Cache-safe: injected as a system_injected user message in the conversation
|
|
542
|
+
# turns, never touching the system prompt.
|
|
543
|
+
def inject_chunk_index_if_needed
|
|
544
|
+
# Collect all compressed_summary messages that carry a chunk_path
|
|
545
|
+
chunk_msgs = @history.to_a.select { |m| m[:compressed_summary] && m[:chunk_path] }
|
|
546
|
+
return if chunk_msgs.empty?
|
|
547
|
+
|
|
548
|
+
# Skip if we already injected an index for this exact chunk count
|
|
549
|
+
return if @history.last_injected_chunk_count == chunk_msgs.size
|
|
550
|
+
|
|
551
|
+
# Remove any previously injected chunk index (stale — chunk count changed)
|
|
552
|
+
@history.delete_where { |m| m[:chunk_index] }
|
|
553
|
+
|
|
554
|
+
# Build index card lines
|
|
555
|
+
lines = ["## Previous Session Archives (#{chunk_msgs.size} chunk#{"s" if chunk_msgs.size > 1} available)\n"]
|
|
556
|
+
chunk_msgs.each_with_index do |msg, i|
|
|
557
|
+
path = msg[:chunk_path].to_s
|
|
558
|
+
topics = read_chunk_topics(path)
|
|
559
|
+
turns = read_chunk_message_count(path)
|
|
560
|
+
lines << "[CHUNK-#{i + 1}] #{path}"
|
|
561
|
+
lines << " Topics: #{topics}" if topics
|
|
562
|
+
lines << " Turns: #{turns}" if turns
|
|
563
|
+
lines << ""
|
|
564
|
+
end
|
|
565
|
+
lines << "Use file_reader to load a chunk file when you need original conversation details."
|
|
566
|
+
|
|
567
|
+
@history.append({
|
|
568
|
+
role: "user",
|
|
569
|
+
content: lines.join("\n"),
|
|
570
|
+
system_injected: true,
|
|
571
|
+
chunk_index: true,
|
|
572
|
+
chunk_count: chunk_msgs.size
|
|
573
|
+
})
|
|
574
|
+
end
|
|
575
|
+
|
|
576
|
+
# Read the `topics` field from a chunk MD file's YAML front matter.
|
|
577
|
+
# Returns nil if the file is missing or has no topics field.
|
|
578
|
+
private def read_chunk_topics(chunk_path)
|
|
579
|
+
return nil unless chunk_path && File.exist?(chunk_path)
|
|
580
|
+
File.foreach(chunk_path) do |line|
|
|
581
|
+
return line.sub(/^topics:\s*/, "").strip if line.start_with?("topics:")
|
|
582
|
+
break if line.strip == "---" && $. > 1 # end of front matter
|
|
583
|
+
end
|
|
584
|
+
nil
|
|
585
|
+
rescue
|
|
586
|
+
nil
|
|
587
|
+
end
|
|
588
|
+
|
|
589
|
+
# Read the `message_count` field from a chunk MD file's YAML front matter.
|
|
590
|
+
# Returns nil if the file is missing or has no message_count field.
|
|
591
|
+
private def read_chunk_message_count(chunk_path)
|
|
592
|
+
return nil unless chunk_path && File.exist?(chunk_path)
|
|
593
|
+
File.foreach(chunk_path) do |line|
|
|
594
|
+
return line.sub(/^message_count:\s*/, "").strip.to_i if line.start_with?("message_count:")
|
|
595
|
+
break if line.strip == "---" && $. > 1
|
|
596
|
+
end
|
|
597
|
+
nil
|
|
598
|
+
rescue
|
|
599
|
+
nil
|
|
600
|
+
end
|
|
532
601
|
end
|
|
533
602
|
end
|
|
534
603
|
end
|
|
@@ -255,7 +255,7 @@ module Clacky
|
|
|
255
255
|
transient: transient
|
|
256
256
|
})
|
|
257
257
|
|
|
258
|
-
@ui&.show_info("Injected skill content for /#{skill.identifier}#{skill.name_zh ? " (#{skill.name_zh})"
|
|
258
|
+
@ui&.show_info("Injected skill content for /#{skill.identifier}#{skill.name_zh.to_s.empty? ? "" : " (#{skill.name_zh})"}")
|
|
259
259
|
end
|
|
260
260
|
|
|
261
261
|
|
|
@@ -405,7 +405,7 @@ module Clacky
|
|
|
405
405
|
|
|
406
406
|
# Log which model the subagent is actually using (may differ from requested
|
|
407
407
|
# when "lite" falls back to default due to no lite model configured)
|
|
408
|
-
@ui&.show_info("Subagent start: #{skill.identifier}#{skill.name_zh ? " (#{skill.name_zh})"
|
|
408
|
+
@ui&.show_info("Subagent start: #{skill.identifier}#{skill.name_zh.to_s.empty? ? "" : " (#{skill.name_zh})"} [#{subagent.current_model_info[:model]}]")
|
|
409
409
|
|
|
410
410
|
# Run subagent with the actual task as the sole user turn.
|
|
411
411
|
# If the user typed the skill command with no arguments (e.g. "/jade-appraisal"),
|
data/lib/clacky/agent.rb
CHANGED
|
@@ -212,6 +212,9 @@ module Clacky
|
|
|
212
212
|
# Inject session context (date + model) if not yet present or date has changed
|
|
213
213
|
inject_session_context_if_needed
|
|
214
214
|
|
|
215
|
+
# Inject chunk index card if archived chunks exist and index is stale
|
|
216
|
+
inject_chunk_index_if_needed
|
|
217
|
+
|
|
215
218
|
# Split files into vision images and disk files; downgrade oversized images to disk
|
|
216
219
|
image_files, disk_files = partition_files(Array(files))
|
|
217
220
|
vision_images, downgraded = resolve_vision_images(image_files)
|
data/lib/clacky/brand_config.rb
CHANGED
|
@@ -664,11 +664,36 @@ module Clacky
|
|
|
664
664
|
# installed and that have a newer version available.
|
|
665
665
|
# New skills are never auto-installed — the user must click Install/Update
|
|
666
666
|
# explicitly from the Brand Skills panel.
|
|
667
|
+
installed = installed_brand_skills
|
|
667
668
|
skills_needing_update = result[:skills].select { |s| s["needs_update"] }
|
|
668
669
|
results = skills_needing_update.map do |skill_info|
|
|
669
670
|
install_brand_skill!(skill_info)
|
|
670
671
|
end
|
|
671
672
|
|
|
673
|
+
# Even when the version hasn't changed, display metadata (name_zh,
|
|
674
|
+
# description_zh, description) may have been updated on the platform.
|
|
675
|
+
# Patch brand_skills.json in-place without re-downloading the ZIP.
|
|
676
|
+
result[:skills].each do |skill_info|
|
|
677
|
+
name = skill_info["name"]
|
|
678
|
+
next unless installed.key?(name)
|
|
679
|
+
next if skill_info["needs_update"] # already being reinstalled above
|
|
680
|
+
|
|
681
|
+
local = installed[name]
|
|
682
|
+
next if local["name_zh"] == skill_info["name_zh"].to_s &&
|
|
683
|
+
local["description_zh"] == skill_info["description_zh"].to_s &&
|
|
684
|
+
local["description"] == skill_info["description"].to_s
|
|
685
|
+
|
|
686
|
+
# Metadata changed — update brand_skills.json without reinstalling.
|
|
687
|
+
record_installed_skill(
|
|
688
|
+
name,
|
|
689
|
+
local["version"],
|
|
690
|
+
skill_info["description"].to_s,
|
|
691
|
+
encrypted: local["encrypted"] != false,
|
|
692
|
+
description_zh: skill_info["description_zh"].to_s,
|
|
693
|
+
name_zh: skill_info["name_zh"].to_s
|
|
694
|
+
)
|
|
695
|
+
end
|
|
696
|
+
|
|
672
697
|
on_complete&.call(results)
|
|
673
698
|
rescue StandardError
|
|
674
699
|
# Background sync failures are intentionally swallowed — the agent
|
data/lib/clacky/cli.rb
CHANGED
|
@@ -709,8 +709,21 @@ module Clacky
|
|
|
709
709
|
sleep 0.1
|
|
710
710
|
# Clear output area
|
|
711
711
|
ui_controller.layout.clear_output
|
|
712
|
+
# Cancel old idle timer before replacing agent to avoid stale-agent compression
|
|
713
|
+
idle_timer.cancel
|
|
712
714
|
# Clear session by creating a new agent
|
|
713
715
|
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, source: :manual)
|
|
716
|
+
# Rebuild idle timer bound to the new agent
|
|
717
|
+
idle_timer = Clacky::IdleCompressionTimer.new(
|
|
718
|
+
agent: agent,
|
|
719
|
+
session_manager: session_manager,
|
|
720
|
+
logger: ->(msg, level:) { ui_controller.log(msg, level: level) }
|
|
721
|
+
) do |success|
|
|
722
|
+
if success
|
|
723
|
+
ui_controller.update_sessionbar(tasks: agent.total_tasks, cost: agent.total_cost)
|
|
724
|
+
end
|
|
725
|
+
ui_controller.set_idle_status
|
|
726
|
+
end
|
|
714
727
|
ui_controller.show_info("Session cleared. Starting fresh.")
|
|
715
728
|
# Update session bar with reset values
|
|
716
729
|
ui_controller.update_sessionbar(tasks: agent.total_tasks, cost: agent.total_cost)
|
data/lib/clacky/client.rb
CHANGED
|
@@ -114,12 +114,25 @@ module Clacky
|
|
|
114
114
|
|
|
115
115
|
# ── Prompt-caching support ────────────────────────────────────────────────
|
|
116
116
|
|
|
117
|
-
# Returns true for Claude
|
|
117
|
+
# Returns true for Claude models that support prompt caching (gen 3.5+ or gen 4+).
|
|
118
|
+
#
|
|
119
|
+
# Handles both direct model names (e.g. "claude-haiku-4-5") and
|
|
120
|
+
# Clacky AI Bedrock proxy names with "abs-" prefix (e.g. "abs-claude-haiku-4-5").
|
|
121
|
+
#
|
|
122
|
+
# Why only Claude models:
|
|
123
|
+
# - MiniMax uses automatic server-side caching (no cache_control needed from client)
|
|
124
|
+
# - Kimi uses a proprietary prompt_cache_key param, not cache_control
|
|
125
|
+
# - MiMo has no documented caching API
|
|
126
|
+
# - Only Claude (direct, OpenRouter, or ClackyAI Bedrock proxy) consumes our
|
|
127
|
+
# cache_control / cachePoint markers
|
|
118
128
|
def supports_prompt_caching?(model)
|
|
119
|
-
|
|
129
|
+
# Strip ClackyAI Bedrock proxy prefix before matching
|
|
130
|
+
model_str = model.to_s.downcase.sub(/^abs-/, "")
|
|
120
131
|
return false unless model_str.include?("claude")
|
|
121
132
|
|
|
122
|
-
|
|
133
|
+
# Match Claude gen 3.5+ (3.5/3.6/3.7…) or gen 4+ in any name format:
|
|
134
|
+
# claude-3.5-sonnet-... claude-3-7-sonnet claude-haiku-4-5 claude-sonnet-4-6
|
|
135
|
+
model_str.match?(/claude(?:-3[-.]?[5-9]|.*-[4-9][-.]|.*-[4-9]$|-[4-9][-.]|-[4-9]$|-sonnet-[34])/)
|
|
123
136
|
end
|
|
124
137
|
|
|
125
138
|
|
|
@@ -97,10 +97,6 @@ module Clacky
|
|
|
97
97
|
emit("info", message: message)
|
|
98
98
|
end
|
|
99
99
|
|
|
100
|
-
def show_idle_status(phase:, message:)
|
|
101
|
-
emit("idle_status", phase: phase.to_s, message: message)
|
|
102
|
-
end
|
|
103
|
-
|
|
104
100
|
def show_warning(message)
|
|
105
101
|
emit("warning", message: message)
|
|
106
102
|
end
|
|
@@ -119,15 +115,25 @@ module Clacky
|
|
|
119
115
|
|
|
120
116
|
# === Progress ===
|
|
121
117
|
|
|
122
|
-
def show_progress(message = nil, prefix_newline: true,
|
|
123
|
-
@progress_start_time = Time.now
|
|
124
|
-
|
|
118
|
+
def show_progress(message = nil, prefix_newline: true, progress_type: "thinking", phase: "active", metadata: {})
|
|
119
|
+
@progress_start_time = Time.now if phase == "active"
|
|
120
|
+
|
|
121
|
+
data = {
|
|
122
|
+
message: message,
|
|
123
|
+
progress_type: progress_type,
|
|
124
|
+
phase: phase,
|
|
125
|
+
status: phase == "active" ? "start" : "stop" # backward compat
|
|
126
|
+
}
|
|
127
|
+
data[:metadata] = metadata unless metadata.empty?
|
|
128
|
+
data[:elapsed] = (Time.now - @progress_start_time).round(1) if phase == "done" && @progress_start_time
|
|
129
|
+
|
|
130
|
+
emit("progress", **data)
|
|
131
|
+
|
|
132
|
+
@progress_start_time = nil if phase == "done"
|
|
125
133
|
end
|
|
126
134
|
|
|
127
135
|
def clear_progress
|
|
128
|
-
|
|
129
|
-
@progress_start_time = nil
|
|
130
|
-
emit("progress", status: "stop", elapsed: elapsed)
|
|
136
|
+
show_progress(progress_type: "thinking", phase: "done")
|
|
131
137
|
end
|
|
132
138
|
|
|
133
139
|
# === State updates ===
|
|
@@ -12,6 +12,7 @@ module Clacky
|
|
|
12
12
|
task_id created_at system_injected session_context memory_update
|
|
13
13
|
subagent_instructions subagent_result token_usage
|
|
14
14
|
compressed_summary chunk_path truncated transient
|
|
15
|
+
chunk_index chunk_count
|
|
15
16
|
].freeze
|
|
16
17
|
|
|
17
18
|
def initialize(messages = [])
|
|
@@ -123,6 +124,13 @@ module Clacky
|
|
|
123
124
|
msg&.dig(:session_date)
|
|
124
125
|
end
|
|
125
126
|
|
|
127
|
+
# Return the chunk_count from the most recently injected chunk index message.
|
|
128
|
+
# Used by inject_chunk_index_if_needed to avoid re-injecting when nothing changed.
|
|
129
|
+
def last_injected_chunk_count
|
|
130
|
+
msg = @messages.reverse.find { |m| m[:chunk_index] }
|
|
131
|
+
msg&.dig(:chunk_count) || 0
|
|
132
|
+
end
|
|
133
|
+
|
|
126
134
|
# Return only real (non-system-injected) user messages.
|
|
127
135
|
def real_user_messages
|
|
128
136
|
@messages.select { |m| m[:role] == "user" && !m[:system_injected] }
|
|
@@ -87,11 +87,6 @@ module Clacky
|
|
|
87
87
|
puts_line("[info] #{message}")
|
|
88
88
|
end
|
|
89
89
|
|
|
90
|
-
def show_idle_status(phase:, message:)
|
|
91
|
-
# In plain mode, just print the final state
|
|
92
|
-
puts_line("[info] #{message}") if phase.to_s == "end"
|
|
93
|
-
end
|
|
94
|
-
|
|
95
90
|
def show_warning(message)
|
|
96
91
|
puts_line("[warn] #{message}")
|
|
97
92
|
end
|
|
@@ -111,7 +106,7 @@ module Clacky
|
|
|
111
106
|
|
|
112
107
|
# === Progress (no-ops — no spinner in plain mode) ===
|
|
113
108
|
|
|
114
|
-
def show_progress(message = nil, prefix_newline: true,
|
|
109
|
+
def show_progress(message = nil, prefix_newline: true, progress_type: "thinking", phase: "active", metadata: {}); end
|
|
115
110
|
def clear_progress; end
|
|
116
111
|
|
|
117
112
|
# === State updates (no-ops) ===
|
data/lib/clacky/providers.rb
CHANGED
|
@@ -11,13 +11,31 @@ module Clacky
|
|
|
11
11
|
# - api: API type (anthropic-messages, openai-responses, openai-completions)
|
|
12
12
|
# - default_model: Recommended default model
|
|
13
13
|
PRESETS = {
|
|
14
|
+
"openclacky" => {
|
|
15
|
+
"name" => "OpenClacky",
|
|
16
|
+
"base_url" => "https://api.openclacky.com",
|
|
17
|
+
"api" => "bedrock",
|
|
18
|
+
"default_model" => "abs-claude-sonnet-4-5",
|
|
19
|
+
"lite_model" => "abs-claude-haiku-4-5",
|
|
20
|
+
"models" => [
|
|
21
|
+
"abs-claude-opus-4-6",
|
|
22
|
+
"abs-claude-sonnet-4-6",
|
|
23
|
+
"abs-claude-sonnet-4-5",
|
|
24
|
+
"abs-claude-haiku-4-5"
|
|
25
|
+
],
|
|
26
|
+
# Fallback chain: if a model is unavailable, try the next one in order.
|
|
27
|
+
# Keys are primary model names; values are the fallback model to use instead.
|
|
28
|
+
"fallback_models" => {
|
|
29
|
+
"abs-claude-sonnet-4-6" => "abs-claude-sonnet-4-5"
|
|
30
|
+
},
|
|
31
|
+
"website_url" => "https://www.openclacky.com/ai-keys"
|
|
32
|
+
}.freeze,
|
|
14
33
|
|
|
15
34
|
"openrouter" => {
|
|
16
35
|
"name" => "OpenRouter",
|
|
17
36
|
"base_url" => "https://openrouter.ai/api/v1",
|
|
18
37
|
"api" => "openai-responses",
|
|
19
38
|
"default_model" => "anthropic/claude-sonnet-4-6",
|
|
20
|
-
"lite_model" => "anthropic/claude-haiku-4-5",
|
|
21
39
|
"models" => [], # Dynamic - fetched from API
|
|
22
40
|
"website_url" => "https://openrouter.ai/keys"
|
|
23
41
|
}.freeze,
|
|
@@ -49,15 +67,16 @@ module Clacky
|
|
|
49
67
|
"website_url" => "https://console.anthropic.com/settings/keys"
|
|
50
68
|
}.freeze,
|
|
51
69
|
|
|
52
|
-
"clackyai" => {
|
|
53
|
-
"name" => "ClackyAI",
|
|
70
|
+
"clackyai-sea" => {
|
|
71
|
+
"name" => "ClackyAI( Sea )",
|
|
54
72
|
"base_url" => "https://api.clacky.ai",
|
|
55
73
|
"api" => "bedrock",
|
|
56
|
-
"default_model" => "abs-claude-sonnet-4-
|
|
74
|
+
"default_model" => "abs-claude-sonnet-4-5",
|
|
57
75
|
"lite_model" => "abs-claude-haiku-4-5",
|
|
58
76
|
"models" => [
|
|
59
77
|
"abs-claude-opus-4-6",
|
|
60
78
|
"abs-claude-sonnet-4-6",
|
|
79
|
+
"abs-claude-sonnet-4-5",
|
|
61
80
|
"abs-claude-haiku-4-5"
|
|
62
81
|
],
|
|
63
82
|
# Fallback chain: if a model is unavailable, try the next one in order.
|
|
@@ -361,7 +361,9 @@ module Clacky
|
|
|
361
361
|
|
|
362
362
|
send_frame(cmd: cmd, req_id: req_id, body: body)
|
|
363
363
|
|
|
364
|
-
|
|
364
|
+
timeout_thread = Thread.new { sleep 30; queue.push(nil) }
|
|
365
|
+
result = queue.pop
|
|
366
|
+
timeout_thread.kill
|
|
365
367
|
raise "Timeout waiting for ack (req_id=#{req_id}, cmd=#{cmd})" if result.nil?
|
|
366
368
|
|
|
367
369
|
errcode = result["errcode"] || result.dig("body", "errcode")
|
|
@@ -417,11 +417,11 @@ module Clacky
|
|
|
417
417
|
r = text.dup
|
|
418
418
|
r.gsub!(/```[^\n]*\n?([\s\S]*?)```/) { Regexp.last_match(1).strip }
|
|
419
419
|
r.gsub!(/!\[[^\]]*\]\([^)]*\)/, "")
|
|
420
|
-
r.gsub!(/\[([^\]]+)\]\([^)]*\)/, '')
|
|
421
|
-
r.gsub!(/\*\*([^*]+)\*\*/, '')
|
|
422
|
-
r.gsub!(/\*([^*]+)\*/, '')
|
|
423
|
-
r.gsub!(/__([^_]+)__/, '')
|
|
424
|
-
r.gsub!(/_([^_]+)_/, '')
|
|
420
|
+
r.gsub!(/\[([^\]]+)\]\([^)]*\)/, '\\1')
|
|
421
|
+
r.gsub!(/\*\*([^*]+)\*\*/, '\\1')
|
|
422
|
+
r.gsub!(/\*([^*]+)\*/, '\\1')
|
|
423
|
+
r.gsub!(/__([^_]+)__/, '\\1')
|
|
424
|
+
r.gsub!(/_([^_]+)_/, '\\1')
|
|
425
425
|
r.gsub!(/^#+\s+/, "")
|
|
426
426
|
r.gsub!(/^[-*_]{3,}\s*$/, "")
|
|
427
427
|
r.strip
|
|
@@ -208,13 +208,25 @@ module Clacky
|
|
|
208
208
|
|
|
209
209
|
# === Progress ===
|
|
210
210
|
|
|
211
|
-
def show_progress(message = nil, prefix_newline: true,
|
|
212
|
-
@progress_start_time = Time.now
|
|
211
|
+
def show_progress(message = nil, prefix_newline: true, progress_type: "thinking", phase: "active", metadata: {})
|
|
212
|
+
@progress_start_time = Time.now if phase == "active"
|
|
213
213
|
@live_progress_message = message
|
|
214
214
|
# Reset stdout buffer for each new command so re-subscribe only replays current run
|
|
215
|
-
@live_stdout_buffer = []
|
|
216
|
-
|
|
215
|
+
@live_stdout_buffer = [] if phase == "active"
|
|
216
|
+
|
|
217
|
+
data = {
|
|
218
|
+
message: message,
|
|
219
|
+
progress_type: progress_type,
|
|
220
|
+
phase: phase,
|
|
221
|
+
status: phase == "active" ? "start" : "stop" # backward compat
|
|
222
|
+
}
|
|
223
|
+
data[:metadata] = metadata unless metadata.empty?
|
|
224
|
+
data[:elapsed] = (Time.now - @progress_start_time).round(1) if phase == "done" && @progress_start_time
|
|
225
|
+
|
|
226
|
+
emit("progress", **data)
|
|
217
227
|
forward_to_subscribers { |sub| sub.show_progress(message) }
|
|
228
|
+
|
|
229
|
+
@progress_start_time = nil if phase == "done"
|
|
218
230
|
end
|
|
219
231
|
|
|
220
232
|
# Stream shell stdout/stderr lines to the browser while a command is running.
|
|
@@ -230,14 +242,10 @@ module Clacky
|
|
|
230
242
|
end
|
|
231
243
|
|
|
232
244
|
def clear_progress
|
|
233
|
-
elapsed = @progress_start_time ? (Time.now - @progress_start_time).round(1) : 0
|
|
234
|
-
@progress_start_time = nil
|
|
235
|
-
@live_progress_message = nil
|
|
236
245
|
@live_tool_call = nil # command finished — nothing left to replay
|
|
237
246
|
# Keep @live_stdout_buffer intact — it will be reset on the next show_progress call.
|
|
238
247
|
# This allows a brief replay window even after the command finishes.
|
|
239
|
-
|
|
240
|
-
forward_to_subscribers { |sub| sub.clear_progress }
|
|
248
|
+
show_progress(progress_type: "thinking", phase: "done")
|
|
241
249
|
end
|
|
242
250
|
|
|
243
251
|
# Replay in-progress command state to a newly (re-)subscribing browser tab.
|
|
@@ -466,8 +466,7 @@ module Clacky
|
|
|
466
466
|
# Show progress indicator with dynamic elapsed time
|
|
467
467
|
# @param message [String] Progress message (optional, will use random thinking verb if nil)
|
|
468
468
|
# @param prefix_newline [Boolean] Whether to add a blank line before progress (default: true)
|
|
469
|
-
|
|
470
|
-
def show_progress(message = nil, prefix_newline: true, output_buffer: nil)
|
|
469
|
+
def show_progress(message = nil, prefix_newline: true, progress_type: "thinking", phase: "active", metadata: {})
|
|
471
470
|
# Stop any existing progress thread
|
|
472
471
|
stop_progress_thread
|
|
473
472
|
|
|
@@ -476,7 +475,6 @@ module Clacky
|
|
|
476
475
|
|
|
477
476
|
@progress_message = message || Clacky::THINKING_VERBS.sample
|
|
478
477
|
@progress_start_time = Time.now
|
|
479
|
-
@progress_output_buffer = output_buffer
|
|
480
478
|
# Flag used by the progress thread to know when to stop gracefully.
|
|
481
479
|
# Using a flag + join is safe because Thread#kill can interrupt a thread
|
|
482
480
|
# while it holds @render_mutex, causing a permanent deadlock.
|
|
@@ -562,7 +560,6 @@ module Clacky
|
|
|
562
560
|
|
|
563
561
|
# Signal thread to stop without joining — it will exit on next loop tick
|
|
564
562
|
@progress_start_time = nil
|
|
565
|
-
@progress_output_buffer = nil
|
|
566
563
|
@stdout_lines = nil
|
|
567
564
|
@progress_thread_stop = true
|
|
568
565
|
# Detach: let the thread die on its own; we do NOT join here
|
|
@@ -594,7 +591,6 @@ module Clacky
|
|
|
594
591
|
# @render_mutex) and leave the mutex permanently locked.
|
|
595
592
|
def stop_progress_thread
|
|
596
593
|
@progress_start_time = nil
|
|
597
|
-
@progress_output_buffer = nil
|
|
598
594
|
@progress_thread_stop = true
|
|
599
595
|
if @progress_thread&.alive?
|
|
600
596
|
# Join with a short timeout; fall back to kill only as a last resort
|
data/lib/clacky/ui_interface.rb
CHANGED
|
@@ -30,7 +30,11 @@ module Clacky
|
|
|
30
30
|
def log(message, level: :info); end
|
|
31
31
|
|
|
32
32
|
# === Progress ===
|
|
33
|
-
|
|
33
|
+
# Unified progress indicator with type-based display customization.
|
|
34
|
+
# progress_type: "thinking" | "retrying" | "idle_compress" | custom
|
|
35
|
+
# phase: "active" | "done"
|
|
36
|
+
# metadata: extensible hash (e.g., {attempt: 3, total: 10} for retries)
|
|
37
|
+
def show_progress(message = nil, prefix_newline: true, progress_type: "thinking", phase: "active", metadata: {}); end
|
|
34
38
|
def clear_progress; end
|
|
35
39
|
|
|
36
40
|
# === State updates ===
|
|
@@ -39,12 +43,6 @@ module Clacky
|
|
|
39
43
|
def set_working_status; end
|
|
40
44
|
def set_idle_status; end
|
|
41
45
|
|
|
42
|
-
# === Idle compression status ===
|
|
43
|
-
# Emits a two-phase idle compression status update.
|
|
44
|
-
# phase: :start → show "Idle detected. Compressing..." (with spinner)
|
|
45
|
-
# phase: :end → update same element with final result (skipped / compressed)
|
|
46
|
-
def show_idle_status(phase:, message:); end
|
|
47
|
-
|
|
48
46
|
# === Blocking interaction ===
|
|
49
47
|
def request_confirmation(message, default: true); end
|
|
50
48
|
|
data/lib/clacky/version.rb
CHANGED
data/lib/clacky/web/app.css
CHANGED
|
@@ -2012,6 +2012,14 @@ body {
|
|
|
2012
2012
|
padding: 4px 0;
|
|
2013
2013
|
}
|
|
2014
2014
|
|
|
2015
|
+
.skill-ac-empty {
|
|
2016
|
+
padding: 14px 16px;
|
|
2017
|
+
font-size: 13px;
|
|
2018
|
+
color: var(--color-text-tertiary, #888);
|
|
2019
|
+
text-align: left;
|
|
2020
|
+
font-style: italic;
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2015
2023
|
.skill-ac-item {
|
|
2016
2024
|
display: flex;
|
|
2017
2025
|
align-items: baseline;
|
data/lib/clacky/web/app.js
CHANGED
|
@@ -292,6 +292,7 @@ function showConfirmModal(confId, message) {
|
|
|
292
292
|
let _initialRestoreDone = false;
|
|
293
293
|
|
|
294
294
|
WS.onEvent(ev => {
|
|
295
|
+
console.log("[DEBUG] WS event received:", ev.type, ev);
|
|
295
296
|
switch (ev.type) {
|
|
296
297
|
|
|
297
298
|
// ── Internal WS lifecycle ──────────────────────────────────────────
|
|
@@ -446,9 +447,17 @@ WS.onEvent(ev => {
|
|
|
446
447
|
break;
|
|
447
448
|
|
|
448
449
|
case "progress":
|
|
450
|
+
console.log("[DEBUG] progress event:", ev);
|
|
449
451
|
if (ev.session_id !== Sessions.activeId) break;
|
|
450
|
-
if (ev.
|
|
451
|
-
|
|
452
|
+
if (ev.phase === "active" || ev.status === "start") {
|
|
453
|
+
const progress_type = ev.progress_type || "thinking";
|
|
454
|
+
const metadata = ev.metadata || {};
|
|
455
|
+
console.log("[DEBUG] calling showProgress:", { message: ev.message, progress_type, metadata });
|
|
456
|
+
Sessions.showProgress(ev.message, progress_type, metadata);
|
|
457
|
+
} else {
|
|
458
|
+
console.log("[DEBUG] calling clearProgress:", ev.message);
|
|
459
|
+
Sessions.clearProgress(ev.message);
|
|
460
|
+
}
|
|
452
461
|
break;
|
|
453
462
|
|
|
454
463
|
case "complete":
|
|
@@ -471,17 +480,16 @@ WS.onEvent(ev => {
|
|
|
471
480
|
break;
|
|
472
481
|
|
|
473
482
|
// ── Info / errors ──────────────────────────────────────────────────
|
|
474
|
-
case "idle_status":
|
|
475
|
-
// Two-phase: "start" shows a spinner line, "end" updates it in-place
|
|
476
|
-
Sessions.updateIdleStatus(ev.phase, ev.message);
|
|
477
|
-
break;
|
|
478
|
-
|
|
479
483
|
case "info":
|
|
480
484
|
Sessions.appendInfo(ev.message);
|
|
481
485
|
break;
|
|
482
486
|
|
|
483
487
|
case "warning":
|
|
484
|
-
|
|
488
|
+
// Optimize retry messages for better UX
|
|
489
|
+
const friendlyWarning = _transformRetryWarning(ev.message);
|
|
490
|
+
if (friendlyWarning) {
|
|
491
|
+
Sessions.appendInfo(friendlyWarning);
|
|
492
|
+
}
|
|
485
493
|
break;
|
|
486
494
|
|
|
487
495
|
case "success":
|
|
@@ -1204,11 +1212,21 @@ const SkillAC = (() => {
|
|
|
1204
1212
|
|
|
1205
1213
|
_items = scored.map(({ skill }) => skill);
|
|
1206
1214
|
|
|
1207
|
-
if (_items.length === 0) { _hide(); return; }
|
|
1208
|
-
|
|
1209
1215
|
const list = $("skill-autocomplete-list");
|
|
1210
1216
|
list.innerHTML = "";
|
|
1211
1217
|
|
|
1218
|
+
if (_items.length === 0) {
|
|
1219
|
+
// Show empty state instead of hiding the dropdown
|
|
1220
|
+
const emptyEl = document.createElement("div");
|
|
1221
|
+
emptyEl.className = "skill-ac-empty";
|
|
1222
|
+
emptyEl.textContent = I18n.t("skills.ac.empty");
|
|
1223
|
+
list.appendChild(emptyEl);
|
|
1224
|
+
$("skill-autocomplete").style.display = "";
|
|
1225
|
+
_visible = true;
|
|
1226
|
+
_createOverlay();
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1212
1230
|
_items.forEach((skill, idx) => {
|
|
1213
1231
|
const item = document.createElement("div");
|
|
1214
1232
|
item.className = "skill-ac-item" + (idx === _activeIndex ? " active" : "");
|
data/lib/clacky/web/i18n.js
CHANGED
|
@@ -35,6 +35,7 @@ const I18n = (() => {
|
|
|
35
35
|
"chat.input.placeholder": "Message… (Enter to send, Shift+Enter for newline)",
|
|
36
36
|
"chat.btn.send": "Send",
|
|
37
37
|
"chat.thinking": "Thinking…",
|
|
38
|
+
"chat.retrying": "Retrying",
|
|
38
39
|
"chat.history_load_failed": "Could not load history",
|
|
39
40
|
"chat.history_start": "No more history",
|
|
40
41
|
"chat.image_expired": "Expired",
|
|
@@ -188,6 +189,7 @@ const I18n = (() => {
|
|
|
188
189
|
"skills.toggle.disableDesc": "AI can auto-invoke. Disable to prevent auto-triggering (manual use still available)",
|
|
189
190
|
"skills.toggle.enableDesc": "AI cannot auto-invoke. Enable to allow auto-triggering",
|
|
190
191
|
"skills.toggleError": "Error: ",
|
|
192
|
+
"skills.ac.empty": "No skills available",
|
|
191
193
|
"skills.upload.uploading": "Uploading…",
|
|
192
194
|
"skills.upload.uploaded": "Uploaded",
|
|
193
195
|
"skills.upload.upload": "Upload",
|
|
@@ -362,6 +364,7 @@ const I18n = (() => {
|
|
|
362
364
|
"chat.input.placeholder": "输入消息…(Enter 发送,Shift+Enter 换行)",
|
|
363
365
|
"chat.btn.send": "发送",
|
|
364
366
|
"chat.thinking": "思考中…",
|
|
367
|
+
"chat.retrying": "正在重试",
|
|
365
368
|
"chat.history_load_failed": "历史记录加载失败",
|
|
366
369
|
"chat.history_start": "没有更多历史了",
|
|
367
370
|
"chat.image_expired": "已过期",
|
|
@@ -515,6 +518,7 @@ const I18n = (() => {
|
|
|
515
518
|
"skills.toggle.disableDesc": "AI 可自动调用。关闭后 AI 不会主动触发(手动使用仍可用)",
|
|
516
519
|
"skills.toggle.enableDesc": "AI 不会自动调用。开启后允许 AI 主动触发",
|
|
517
520
|
"skills.toggleError": "错误:",
|
|
521
|
+
"skills.ac.empty": "暂无可用技能",
|
|
518
522
|
"skills.upload.uploading": "上传中…",
|
|
519
523
|
"skills.upload.uploaded": "已上传",
|
|
520
524
|
"skills.upload.upload": "上传",
|
|
@@ -740,3 +744,56 @@ const I18n = (() => {
|
|
|
740
744
|
// ── Public API ─────────────────────────────────────────────────────────────
|
|
741
745
|
return { lang, setLang, t, applyAll };
|
|
742
746
|
})();
|
|
747
|
+
|
|
748
|
+
// ── Thinking Verbs for Progress Animation ──────────────────────────────────
|
|
749
|
+
const THINKING_VERBS = {
|
|
750
|
+
en: [
|
|
751
|
+
"Cogitating",
|
|
752
|
+
"Pondering",
|
|
753
|
+
"Ruminating",
|
|
754
|
+
"Deliberating",
|
|
755
|
+
"Contemplating",
|
|
756
|
+
"Flibbertigibbeting",
|
|
757
|
+
"Percolating",
|
|
758
|
+
"Noodling",
|
|
759
|
+
"Brewing",
|
|
760
|
+
"Marinating",
|
|
761
|
+
"Stewing",
|
|
762
|
+
"Mulling",
|
|
763
|
+
"Processing",
|
|
764
|
+
"Computing",
|
|
765
|
+
"Calculating",
|
|
766
|
+
"Analyzing",
|
|
767
|
+
"Synthesizing",
|
|
768
|
+
"Ideating",
|
|
769
|
+
"Brainstorming",
|
|
770
|
+
"Reasoning"
|
|
771
|
+
],
|
|
772
|
+
zh: [
|
|
773
|
+
"思考中",
|
|
774
|
+
"琢磨中",
|
|
775
|
+
"推敲中",
|
|
776
|
+
"酝酿中",
|
|
777
|
+
"沉思中",
|
|
778
|
+
"冥想中",
|
|
779
|
+
"盘算中",
|
|
780
|
+
"权衡中",
|
|
781
|
+
"构思中",
|
|
782
|
+
"揣摩中",
|
|
783
|
+
"运算中",
|
|
784
|
+
"分析中",
|
|
785
|
+
"综合中",
|
|
786
|
+
"理解中",
|
|
787
|
+
"头脑风暴中",
|
|
788
|
+
"推理中",
|
|
789
|
+
"整理思绪中",
|
|
790
|
+
"组织语言中"
|
|
791
|
+
]
|
|
792
|
+
};
|
|
793
|
+
|
|
794
|
+
// Get a random thinking verb based on current language
|
|
795
|
+
function getRandomThinkingVerb() {
|
|
796
|
+
const lang = I18n.lang();
|
|
797
|
+
const verbs = THINKING_VERBS[lang] || THINKING_VERBS.en;
|
|
798
|
+
return verbs[Math.floor(Math.random() * verbs.length)];
|
|
799
|
+
}
|
data/lib/clacky/web/sessions.js
CHANGED
|
@@ -1314,52 +1314,81 @@ const Sessions = (() => {
|
|
|
1314
1314
|
_scrollToBottomIfNeeded(messages);
|
|
1315
1315
|
},
|
|
1316
1316
|
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
if (phase === "start") {
|
|
1322
|
-
Sessions.collapseToolGroup();
|
|
1323
|
-
const el = document.createElement("div");
|
|
1324
|
-
el.className = "msg msg-info msg-idle-status";
|
|
1325
|
-
el.textContent = "⟳ " + text;
|
|
1326
|
-
messages.appendChild(el);
|
|
1327
|
-
Sessions._idleStatusEl = el;
|
|
1328
|
-
} else {
|
|
1329
|
-
// phase "end" — update existing element or create a new one if start was missed
|
|
1330
|
-
if (Sessions._idleStatusEl) {
|
|
1331
|
-
Sessions._idleStatusEl.textContent = "· " + text;
|
|
1332
|
-
Sessions._idleStatusEl.classList.add("msg-idle-done");
|
|
1333
|
-
Sessions._idleStatusEl = null;
|
|
1334
|
-
} else {
|
|
1335
|
-
Sessions.appendInfo("· " + text);
|
|
1336
|
-
}
|
|
1337
|
-
}
|
|
1338
|
-
_scrollToBottomIfNeeded(messages);
|
|
1339
|
-
},
|
|
1340
|
-
|
|
1341
|
-
_idleStatusEl: null,
|
|
1317
|
+
_progressEl: null,
|
|
1318
|
+
_progressInterval: null,
|
|
1319
|
+
_progressStartTime: null,
|
|
1320
|
+
_progressType: null,
|
|
1342
1321
|
|
|
1343
|
-
showProgress(text) {
|
|
1322
|
+
showProgress(text, progress_type = "thinking", metadata = {}) {
|
|
1323
|
+
console.log("[DEBUG] showProgress called:", { text, progress_type, metadata });
|
|
1344
1324
|
Sessions.clearProgress();
|
|
1325
|
+
|
|
1326
|
+
Sessions._progressType = progress_type;
|
|
1327
|
+
Sessions._progressStartTime = Date.now();
|
|
1328
|
+
|
|
1345
1329
|
const messages = $("messages");
|
|
1346
1330
|
const el = document.createElement("div");
|
|
1347
|
-
el.className
|
|
1348
|
-
|
|
1331
|
+
el.className = "progress-msg";
|
|
1332
|
+
|
|
1333
|
+
// Choose display text based on type
|
|
1334
|
+
let displayText;
|
|
1335
|
+
if (progress_type === "thinking") {
|
|
1336
|
+
displayText = text || getRandomThinkingVerb();
|
|
1337
|
+
console.log("[DEBUG] thinking verb:", displayText);
|
|
1338
|
+
} else if (progress_type === "retrying") {
|
|
1339
|
+
const { attempt, total } = metadata;
|
|
1340
|
+
// Show error reason + retry count
|
|
1341
|
+
if (text && attempt && total) {
|
|
1342
|
+
displayText = `${I18n.t("chat.retrying")}: ${text} (${attempt}/${total})`;
|
|
1343
|
+
} else if (attempt && total) {
|
|
1344
|
+
displayText = `${I18n.t("chat.retrying")} (${attempt}/${total})`;
|
|
1345
|
+
} else {
|
|
1346
|
+
displayText = text || I18n.t("chat.retrying");
|
|
1347
|
+
}
|
|
1348
|
+
} else if (progress_type === "idle_compress") {
|
|
1349
|
+
displayText = text || "Compressing...";
|
|
1350
|
+
} else {
|
|
1351
|
+
displayText = text || I18n.t("chat.thinking");
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
el.textContent = "⟳ " + displayText;
|
|
1355
|
+
console.log("[DEBUG] appending progress element:", el.textContent);
|
|
1349
1356
|
messages.appendChild(el);
|
|
1350
1357
|
Sessions._progressEl = el;
|
|
1351
1358
|
_scrollToBottomIfNeeded(messages);
|
|
1359
|
+
|
|
1360
|
+
// Start elapsed time counter (update every second)
|
|
1361
|
+
Sessions._progressInterval = setInterval(() => {
|
|
1362
|
+
const elapsed = Math.floor((Date.now() - Sessions._progressStartTime) / 1000);
|
|
1363
|
+
if (Sessions._progressEl) {
|
|
1364
|
+
Sessions._progressEl.textContent = `⟳ ${displayText}… (${elapsed}s)`;
|
|
1365
|
+
}
|
|
1366
|
+
}, 1000);
|
|
1352
1367
|
},
|
|
1353
1368
|
|
|
1354
|
-
clearProgress() {
|
|
1369
|
+
clearProgress(finalMessage = null) {
|
|
1370
|
+
console.log("[DEBUG] clearProgress called:", finalMessage);
|
|
1371
|
+
// Clear interval timer
|
|
1372
|
+
if (Sessions._progressInterval) {
|
|
1373
|
+
clearInterval(Sessions._progressInterval);
|
|
1374
|
+
Sessions._progressInterval = null;
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
// Remove progress element
|
|
1355
1378
|
if (Sessions._progressEl) {
|
|
1356
1379
|
Sessions._progressEl.remove();
|
|
1357
1380
|
Sessions._progressEl = null;
|
|
1358
1381
|
}
|
|
1382
|
+
|
|
1383
|
+
// Show final message if provided (for idle_compress, etc.)
|
|
1384
|
+
if (finalMessage && Sessions._progressType !== "thinking") {
|
|
1385
|
+
Sessions.appendInfo(`· ${finalMessage}`);
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
Sessions._progressStartTime = null;
|
|
1389
|
+
Sessions._progressType = null;
|
|
1359
1390
|
},
|
|
1360
1391
|
|
|
1361
|
-
_progressEl: null,
|
|
1362
|
-
|
|
1363
1392
|
// ── Create ─────────────────────────────────────────────────────────────
|
|
1364
1393
|
|
|
1365
1394
|
/** Create a new session and navigate to it. */
|