openclacky 0.9.30 → 0.9.31
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 +20 -0
- data/lib/clacky/agent/session_serializer.rb +2 -1
- data/lib/clacky/agent.rb +17 -4
- data/lib/clacky/brand_config.rb +23 -0
- data/lib/clacky/default_skills/onboard/SKILL.md +15 -7
- data/lib/clacky/plain_ui_controller.rb +19 -0
- data/lib/clacky/providers.rb +12 -1
- data/lib/clacky/server/http_server.rb +13 -1
- data/lib/clacky/server/web_ui_controller.rb +43 -19
- data/lib/clacky/tools/shell.rb +36 -17
- data/lib/clacky/ui2/ui_controller.rb +45 -8
- data/lib/clacky/utils/arguments_parser.rb +7 -2
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +247 -51
- data/lib/clacky/web/app.js +5 -0
- data/lib/clacky/web/brand.js +21 -3
- data/lib/clacky/web/creator.js +13 -2
- data/lib/clacky/web/i18n.js +41 -15
- data/lib/clacky/web/index.html +38 -20
- data/lib/clacky/web/sessions.js +63 -1
- data/lib/clacky/web/settings.js +32 -0
- data/lib/clacky/web/skills.js +61 -1
- 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: eb05bb9cc5c24901331584bde84b85b264536fa838001bcf113f41df5a96dbce
|
|
4
|
+
data.tar.gz: a8bde52bacb92f46a894582ad9b00afa2a325323b464194fbbd744a4cbbddc77
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3684277987068171be446db5ab6efa3fe33304714d963cd757cc2102477911a13dc2b68f7dec2952d020f1bfd6427d8510469214dcdc8c1e8666995c93ddb098
|
|
7
|
+
data.tar.gz: 7a84b5fc71f3beb237ed07475bd7a72dac6162c9b519e732e1aba2cafe0af05e3d8f1a4d90e3ec857a084a158113720213a873ea73c997ebb553eea39e61964a
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.9.31] - 2026-04-18
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- GLM (智谱) model provider support — select GLM models directly from the provider settings
|
|
14
|
+
- Claude Opus 4.7 model option in the built-in provider list
|
|
15
|
+
- Skill Creator UI — create and edit skills from the Web interface with a visual editor
|
|
16
|
+
- Interactive feedback cards — `request_user_feedback` now renders as a styled interactive card in all UIs (Web, UI2, plain), instead of plain text
|
|
17
|
+
- Brand deactivation — white-label brand can now be toggled on/off from the settings page
|
|
18
|
+
- Empty skill placeholder — shows a friendly message when no skills are installed yet
|
|
19
|
+
|
|
20
|
+
### Improved
|
|
21
|
+
- Shell tool large output handling — when a shell command waits for input or times out with large output, the output is now properly truncated and saved to temp files so the agent can still read the full content
|
|
22
|
+
- Chinese UI translations expanded with new thinkverbose labels
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
- Bedrock streaming truncation recovery — when a tool call's arguments are truncated by the API, the broken assistant message is now retracted from history and the agent retries cleanly instead of crashing
|
|
26
|
+
- First session scroll position in the Web UI sidebar
|
|
27
|
+
- Idle status indicator in UI2
|
|
28
|
+
- Channels page spacing and skill creator label alignment in Web UI
|
|
29
|
+
|
|
10
30
|
## [0.9.30] - 2026-04-16
|
|
11
31
|
|
|
12
32
|
### Added
|
|
@@ -414,7 +414,8 @@ module Clacky
|
|
|
414
414
|
parts = []
|
|
415
415
|
parts << "**Context:** #{context.strip}" << "" unless context.strip.empty?
|
|
416
416
|
parts << "**Question:** #{question.strip}"
|
|
417
|
-
|
|
417
|
+
# Guard: options must be an Array to iterate with each_with_index
|
|
418
|
+
if options.is_a?(Array) && !options.empty?
|
|
418
419
|
parts << "" << "**Options:**"
|
|
419
420
|
options.each_with_index { |opt, i| parts << " #{i + 1}. #{opt}" }
|
|
420
421
|
end
|
data/lib/clacky/agent.rb
CHANGED
|
@@ -698,11 +698,13 @@ module Clacky
|
|
|
698
698
|
@ui&.update_todos(@todos.dup)
|
|
699
699
|
end
|
|
700
700
|
|
|
701
|
-
# Special handling for request_user_feedback:
|
|
701
|
+
# Special handling for request_user_feedback: emit as interactive feedback card
|
|
702
702
|
if call[:name] == "request_user_feedback"
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
703
|
+
# Pass the raw call arguments to show_tool_call so the WebUI controller
|
|
704
|
+
# can extract question/context/options and emit a "request_feedback" event
|
|
705
|
+
# (renders as a clickable card in the browser).
|
|
706
|
+
# Fallback UIs (terminal, IM channels) receive the formatted text message.
|
|
707
|
+
@ui&.show_tool_call(call[:name], call[:arguments])
|
|
706
708
|
|
|
707
709
|
if @config.permission_mode == :auto_approve
|
|
708
710
|
# auto_approve means no human is watching (unattended/scheduled tasks).
|
|
@@ -734,6 +736,17 @@ module Clacky
|
|
|
734
736
|
}
|
|
735
737
|
Clacky::Logger.error("tool_execution_error", tool: call[:name], error: e)
|
|
736
738
|
|
|
739
|
+
# If arguments were malformed/truncated (e.g. Bedrock streaming truncation),
|
|
740
|
+
# retract the bad assistant message from history so the next LLM call gets a
|
|
741
|
+
# fresh context rather than re-reading a cached broken tool call.
|
|
742
|
+
# Also skip adding a tool_result — without the assistant message there is no
|
|
743
|
+
# tool_call to pair with, and sending an orphan tool_result breaks the API.
|
|
744
|
+
if e.is_a?(Utils::BadArgumentsError)
|
|
745
|
+
size_before = @history.size
|
|
746
|
+
@history.pop_while { |m| m[:role] == "assistant" && m[:tool_calls]&.any? { |tc| tc[:id] == call[:id] } }
|
|
747
|
+
next if @history.size < size_before # message was retracted, skip tool_result
|
|
748
|
+
end
|
|
749
|
+
|
|
737
750
|
@hooks.trigger(:on_tool_error, call, e)
|
|
738
751
|
@ui&.show_tool_error(e)
|
|
739
752
|
# Use build_denied_result with system_injected=true so LLM knows it can retry
|
data/lib/clacky/brand_config.rb
CHANGED
|
@@ -135,6 +135,29 @@ module Clacky
|
|
|
135
135
|
FileUtils.chmod(0o600, BRAND_FILE)
|
|
136
136
|
end
|
|
137
137
|
|
|
138
|
+
# Remove the local license binding and wipe all brand-related fields from disk.
|
|
139
|
+
# Brand skills installed from this license are also cleared.
|
|
140
|
+
# Returns { success: true }.
|
|
141
|
+
def deactivate!
|
|
142
|
+
clear_brand_skills!
|
|
143
|
+
FileUtils.rm_f(BRAND_FILE)
|
|
144
|
+
# Reset all in-memory state so this instance is clean after the call.
|
|
145
|
+
@product_name = nil
|
|
146
|
+
@package_name = nil
|
|
147
|
+
@logo_url = nil
|
|
148
|
+
@support_contact = nil
|
|
149
|
+
@support_qr_url = nil
|
|
150
|
+
@theme_color = nil
|
|
151
|
+
@homepage_url = nil
|
|
152
|
+
@license_key = nil
|
|
153
|
+
@license_activated_at = nil
|
|
154
|
+
@license_expires_at = nil
|
|
155
|
+
@license_last_heartbeat = nil
|
|
156
|
+
@license_user_id = nil
|
|
157
|
+
@device_id = nil
|
|
158
|
+
{ success: true }
|
|
159
|
+
end
|
|
160
|
+
|
|
138
161
|
# Activate the license against the OpenClacky Cloud API using HMAC proof.
|
|
139
162
|
# Returns a result hash: { success: bool, message: String, data: Hash }
|
|
140
163
|
def activate!(license_key)
|
|
@@ -41,21 +41,21 @@ Example (Chinese):
|
|
|
41
41
|
### 2. Ask the user to name the AI (card)
|
|
42
42
|
|
|
43
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
|
|
44
|
+
Offer a few fun suggestions as options. The user can also ignore the options and type any name directly.
|
|
45
45
|
|
|
46
46
|
If `lang == "zh"`, use:
|
|
47
47
|
```json
|
|
48
48
|
{
|
|
49
|
-
"question": "先来点有意思的 ——
|
|
50
|
-
"options": ["
|
|
49
|
+
"question": "先来点有意思的 —— 你想叫我什么名字?",
|
|
50
|
+
"options": ["摸鱼王", "老六", "夜猫子", "话唠", "包打听", "碎碎念", "掌柜的"]
|
|
51
51
|
}
|
|
52
52
|
```
|
|
53
53
|
|
|
54
54
|
Otherwise (English):
|
|
55
55
|
```json
|
|
56
56
|
{
|
|
57
|
-
"question": "Let's start with something fun — what would you like to call me?
|
|
58
|
-
"options": ["
|
|
57
|
+
"question": "Let's start with something fun — what would you like to call me?",
|
|
58
|
+
"options": ["Nox", "Sable", "Remy", "Vex", "Pip", "Zola", "Bex"]
|
|
59
59
|
}
|
|
60
60
|
```
|
|
61
61
|
|
|
@@ -108,7 +108,11 @@ Call `request_user_feedback` again. This is where we learn about the user themse
|
|
|
108
108
|
If `lang == "zh"`, use:
|
|
109
109
|
```json
|
|
110
110
|
{
|
|
111
|
-
"question": "那你呢?随便聊聊自己吧 ——
|
|
111
|
+
"question": "那你呢?随便聊聊自己吧 —— 全部可选,填多少都行:
|
|
112
|
+
- 你的名字(我该怎么称呼你?)
|
|
113
|
+
- 职业
|
|
114
|
+
- 最希望用 AI 做什么
|
|
115
|
+
- 社交 / 作品链接(GitHub、微博、个人网站等)—— 我会读取公开信息来更了解你",
|
|
112
116
|
"options": []
|
|
113
117
|
}
|
|
114
118
|
```
|
|
@@ -116,7 +120,11 @@ If `lang == "zh"`, use:
|
|
|
116
120
|
Otherwise (English):
|
|
117
121
|
```json
|
|
118
122
|
{
|
|
119
|
-
"question": "Now a bit about you — all optional, skip anything you like
|
|
123
|
+
"question": "Now a bit about you — all optional, skip anything you like.
|
|
124
|
+
- Your name (what should I call you?)
|
|
125
|
+
- Occupation
|
|
126
|
+
- What you want to use AI for most
|
|
127
|
+
- Social / portfolio links (GitHub, Twitter/X, personal site…) — I'll read them to learn about you",
|
|
120
128
|
"options": []
|
|
121
129
|
}
|
|
122
130
|
```
|
|
@@ -23,6 +23,25 @@ module Clacky
|
|
|
23
23
|
|
|
24
24
|
def show_tool_call(name, args)
|
|
25
25
|
args_data = args.is_a?(String) ? (JSON.parse(args) rescue args) : args
|
|
26
|
+
|
|
27
|
+
# Special handling for request_user_feedback — display as a readable prompt
|
|
28
|
+
if name.to_s == "request_user_feedback"
|
|
29
|
+
question = args_data.is_a?(Hash) ? (args_data[:question] || args_data["question"]).to_s : ""
|
|
30
|
+
context = args_data.is_a?(Hash) ? (args_data[:context] || args_data["context"]).to_s : ""
|
|
31
|
+
options = args_data.is_a?(Hash) ? (args_data[:options] || args_data["options"]) : nil
|
|
32
|
+
options = Array(options) if options && !options.is_a?(Array)
|
|
33
|
+
|
|
34
|
+
parts = []
|
|
35
|
+
parts << "**Context:** #{context.strip}" if context && !context.strip.empty?
|
|
36
|
+
parts << "**Question:** #{question.strip}"
|
|
37
|
+
if options && !options.empty?
|
|
38
|
+
parts << "**Options:**"
|
|
39
|
+
options.each_with_index { |opt, i| parts << " #{i + 1}. #{opt}" }
|
|
40
|
+
end
|
|
41
|
+
puts_line(parts.join("\n"))
|
|
42
|
+
return
|
|
43
|
+
end
|
|
44
|
+
|
|
26
45
|
display = case name
|
|
27
46
|
when "shell", "safe_shell"
|
|
28
47
|
cmd = args_data.is_a?(Hash) ? (args_data[:command] || args_data["command"]) : args_data
|
data/lib/clacky/providers.rb
CHANGED
|
@@ -18,6 +18,7 @@ module Clacky
|
|
|
18
18
|
"default_model" => "abs-claude-sonnet-4-5",
|
|
19
19
|
"lite_model" => "abs-claude-haiku-4-5",
|
|
20
20
|
"models" => [
|
|
21
|
+
"abs-claude-opus-4-7",
|
|
21
22
|
"abs-claude-opus-4-6",
|
|
22
23
|
"abs-claude-sonnet-4-6",
|
|
23
24
|
"abs-claude-sonnet-4-5",
|
|
@@ -63,7 +64,7 @@ module Clacky
|
|
|
63
64
|
"base_url" => "https://api.anthropic.com",
|
|
64
65
|
"api" => "anthropic-messages",
|
|
65
66
|
"default_model" => "claude-sonnet-4.6",
|
|
66
|
-
"models" => ["claude-opus-4-6", "claude-sonnet-4.6", "claude-haiku-4.5"],
|
|
67
|
+
"models" => ["claude-opus-4-7", "claude-opus-4-6", "claude-sonnet-4.6", "claude-haiku-4.5"],
|
|
67
68
|
"website_url" => "https://console.anthropic.com/settings/keys"
|
|
68
69
|
}.freeze,
|
|
69
70
|
|
|
@@ -74,6 +75,7 @@ module Clacky
|
|
|
74
75
|
"default_model" => "abs-claude-sonnet-4-5",
|
|
75
76
|
"lite_model" => "abs-claude-haiku-4-5",
|
|
76
77
|
"models" => [
|
|
78
|
+
"abs-claude-opus-4-7",
|
|
77
79
|
"abs-claude-opus-4-6",
|
|
78
80
|
"abs-claude-sonnet-4-6",
|
|
79
81
|
"abs-claude-sonnet-4-5",
|
|
@@ -94,6 +96,15 @@ module Clacky
|
|
|
94
96
|
"default_model" => "mimo-v2-pro",
|
|
95
97
|
"models" => ["mimo-v2-pro", "mimo-v2-omni"],
|
|
96
98
|
"website_url" => "https://platform.xiaomimimo.com/"
|
|
99
|
+
}.freeze,
|
|
100
|
+
|
|
101
|
+
"glm" => {
|
|
102
|
+
"name" => "GLM (ZhipuAI)",
|
|
103
|
+
"base_url" => "https://open.bigmodel.cn/api/paas/v4",
|
|
104
|
+
"api" => "openai-completions",
|
|
105
|
+
"default_model" => "glm-5.1",
|
|
106
|
+
"models" => ["glm-5.1", "glm-5", "glm-5-turbo", "glm-5v-turbo", "glm-4.7"],
|
|
107
|
+
"website_url" => "https://open.bigmodel.cn/usercenter/apikeys"
|
|
97
108
|
}.freeze
|
|
98
109
|
|
|
99
110
|
}.freeze
|
|
@@ -281,7 +281,7 @@ module Clacky
|
|
|
281
281
|
|
|
282
282
|
server.mount_proc("/") do |req, res|
|
|
283
283
|
if req.path == "/" || req.path == "/index.html"
|
|
284
|
-
product_name = Clacky::BrandConfig.load.product_name || "
|
|
284
|
+
product_name = Clacky::BrandConfig.load.product_name || "OpenClacky"
|
|
285
285
|
html = File.read(index_html_path).gsub("{{BRAND_NAME}}", product_name)
|
|
286
286
|
res.status = 200
|
|
287
287
|
res["Content-Type"] = "text/html; charset=utf-8"
|
|
@@ -375,6 +375,7 @@ module Clacky
|
|
|
375
375
|
when ["GET", "/api/store/skills"] then api_store_skills(res)
|
|
376
376
|
when ["GET", "/api/brand/status"] then api_brand_status(res)
|
|
377
377
|
when ["POST", "/api/brand/activate"] then api_brand_activate(req, res)
|
|
378
|
+
when ["DELETE", "/api/brand/license"] then api_brand_deactivate(res)
|
|
378
379
|
when ["GET", "/api/brand/skills"] then api_brand_skills(res)
|
|
379
380
|
when ["GET", "/api/brand"] then api_brand_info(res)
|
|
380
381
|
when ["GET", "/api/creator/skills"] then api_creator_skills(res)
|
|
@@ -680,6 +681,17 @@ module Clacky
|
|
|
680
681
|
end
|
|
681
682
|
end
|
|
682
683
|
|
|
684
|
+
# DELETE /api/brand/license
|
|
685
|
+
# Deactivates (unbinds) the current brand license and clears all brand state.
|
|
686
|
+
# Brand skills are removed from disk. Returns 200 on success.
|
|
687
|
+
private def api_brand_deactivate(res)
|
|
688
|
+
brand = Clacky::BrandConfig.load
|
|
689
|
+
result = brand.deactivate!
|
|
690
|
+
# Reload skill_loader without brand config so brand skills are no longer visible.
|
|
691
|
+
@skill_loader = Clacky::SkillLoader.new(working_dir: nil, brand_config: Clacky::BrandConfig.new({}))
|
|
692
|
+
json_response(res, 200, { ok: true })
|
|
693
|
+
end
|
|
694
|
+
|
|
683
695
|
# GET /api/brand/skills
|
|
684
696
|
# Fetches the brand skills list from the cloud, enriched with local installed version.
|
|
685
697
|
# Returns 200 with skill list, or 403 when license is not activated.
|
|
@@ -92,11 +92,25 @@ module Clacky
|
|
|
92
92
|
end
|
|
93
93
|
|
|
94
94
|
def show_tool_call(name, args)
|
|
95
|
-
# Skip request_user_feedback — its question is already shown as an assistant message
|
|
96
|
-
return if name.to_s == "request_user_feedback"
|
|
97
|
-
|
|
98
95
|
args_data = args.is_a?(String) ? (JSON.parse(args) rescue args) : args
|
|
99
96
|
|
|
97
|
+
# Special handling for request_user_feedback — emit a dedicated UI event
|
|
98
|
+
if name.to_s == "request_user_feedback"
|
|
99
|
+
question = args_data.is_a?(Hash) ? (args_data[:question] || args_data["question"]).to_s : ""
|
|
100
|
+
context = args_data.is_a?(Hash) ? (args_data[:context] || args_data["context"]).to_s : ""
|
|
101
|
+
options = args_data.is_a?(Hash) ? (args_data[:options] || args_data["options"]) : nil
|
|
102
|
+
|
|
103
|
+
# Normalize options to array (guard against malformed data)
|
|
104
|
+
options = Array(options) if options && !options.is_a?(Array)
|
|
105
|
+
|
|
106
|
+
emit("request_feedback",
|
|
107
|
+
question: question,
|
|
108
|
+
context: context,
|
|
109
|
+
options: options || [])
|
|
110
|
+
# Don't forward to IM subscribers — they get the formatted text version already
|
|
111
|
+
return
|
|
112
|
+
end
|
|
113
|
+
|
|
100
114
|
# Generate a human-readable summary using the tool's format_call method
|
|
101
115
|
summary = tool_call_summary(name, args_data)
|
|
102
116
|
|
|
@@ -179,13 +193,6 @@ module Clacky
|
|
|
179
193
|
forward_to_subscribers { |sub| sub.show_info(message) }
|
|
180
194
|
end
|
|
181
195
|
|
|
182
|
-
# Emit a two-phase idle compression status update.
|
|
183
|
-
# The frontend uses the same DOM element for both phases so it renders as one line.
|
|
184
|
-
# phase: :start → show spinner message; phase: :end → update in-place with final result
|
|
185
|
-
def show_idle_status(phase:, message:)
|
|
186
|
-
emit("idle_status", phase: phase.to_s, message: message)
|
|
187
|
-
end
|
|
188
|
-
|
|
189
196
|
def show_warning(message)
|
|
190
197
|
emit("warning", message: message)
|
|
191
198
|
forward_to_subscribers { |sub| sub.show_warning(message) }
|
|
@@ -209,10 +216,21 @@ module Clacky
|
|
|
209
216
|
# === Progress ===
|
|
210
217
|
|
|
211
218
|
def show_progress(message = nil, prefix_newline: true, progress_type: "thinking", phase: "active", metadata: {})
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
219
|
+
if phase == "active"
|
|
220
|
+
@progress_start_time = Time.now
|
|
221
|
+
# Store complete progress state for replay when user switches back to this session
|
|
222
|
+
@live_progress_state = {
|
|
223
|
+
message: message,
|
|
224
|
+
progress_type: progress_type,
|
|
225
|
+
metadata: metadata
|
|
226
|
+
}
|
|
227
|
+
# Reset stdout buffer for each new command so re-subscribe only replays current run
|
|
228
|
+
@live_stdout_buffer = []
|
|
229
|
+
elsif phase == "done"
|
|
230
|
+
# Clear progress state when done
|
|
231
|
+
@live_progress_state = nil
|
|
232
|
+
@progress_start_time = nil
|
|
233
|
+
end
|
|
216
234
|
|
|
217
235
|
data = {
|
|
218
236
|
message: message,
|
|
@@ -225,8 +243,6 @@ module Clacky
|
|
|
225
243
|
|
|
226
244
|
emit("progress", **data)
|
|
227
245
|
forward_to_subscribers { |sub| sub.show_progress(message) }
|
|
228
|
-
|
|
229
|
-
@progress_start_time = nil if phase == "done"
|
|
230
246
|
end
|
|
231
247
|
|
|
232
248
|
# Stream shell stdout/stderr lines to the browser while a command is running.
|
|
@@ -264,9 +280,17 @@ module Clacky
|
|
|
264
280
|
# The frontend's appendToolStdout will attach to the last visible .tool-item
|
|
265
281
|
# even when _liveLastToolItem is null (after the tab re-loaded).
|
|
266
282
|
def replay_live_state
|
|
267
|
-
return unless @
|
|
268
|
-
|
|
269
|
-
|
|
283
|
+
return unless @live_progress_state
|
|
284
|
+
|
|
285
|
+
# Replay complete progress state (not just message)
|
|
286
|
+
state = @live_progress_state
|
|
287
|
+
emit("progress",
|
|
288
|
+
message: state[:message],
|
|
289
|
+
progress_type: state[:progress_type],
|
|
290
|
+
phase: "active",
|
|
291
|
+
status: "start",
|
|
292
|
+
metadata: state[:metadata] || {}
|
|
293
|
+
)
|
|
270
294
|
|
|
271
295
|
buf = @live_stdout_buffer
|
|
272
296
|
emit("tool_stdout", lines: buf) if buf && !buf.empty?
|
data/lib/clacky/tools/shell.rb
CHANGED
|
@@ -397,15 +397,18 @@ module Clacky
|
|
|
397
397
|
end
|
|
398
398
|
|
|
399
399
|
def format_waiting_input_result(command, stdout, stderr, interaction, max_output_lines)
|
|
400
|
+
truncated_stdout = truncate_output(stdout, max_output_lines)
|
|
401
|
+
truncated_stderr = truncate_output(stderr, max_output_lines)
|
|
400
402
|
{
|
|
401
403
|
command: command,
|
|
402
|
-
stdout:
|
|
403
|
-
stderr:
|
|
404
|
+
stdout: truncated_stdout,
|
|
405
|
+
stderr: truncated_stderr,
|
|
404
406
|
exit_code: -2,
|
|
405
407
|
success: false,
|
|
406
408
|
state: 'WAITING_INPUT',
|
|
407
409
|
interaction_type: interaction[:type],
|
|
408
|
-
|
|
410
|
+
interaction: interaction,
|
|
411
|
+
message: format_waiting_message(truncated_stdout, interaction),
|
|
409
412
|
output_truncated: output_truncated?(stdout, stderr, max_output_lines)
|
|
410
413
|
}
|
|
411
414
|
end
|
|
@@ -441,6 +444,11 @@ module Clacky
|
|
|
441
444
|
MSG
|
|
442
445
|
end
|
|
443
446
|
|
|
447
|
+
def extract_last_line(output)
|
|
448
|
+
return "" if output.nil? || output.empty?
|
|
449
|
+
output.lines.last&.strip.to_s[0..200]
|
|
450
|
+
end
|
|
451
|
+
|
|
444
452
|
def format_timeout_result(command, stdout, stderr, elapsed, type, timeout, max_output_lines)
|
|
445
453
|
{
|
|
446
454
|
command: command,
|
|
@@ -506,32 +514,43 @@ module Clacky
|
|
|
506
514
|
MAX_LINE_CHARS = 500
|
|
507
515
|
|
|
508
516
|
def format_result_for_llm(result)
|
|
509
|
-
return result if result[:error]
|
|
517
|
+
return result if result[:error]
|
|
510
518
|
|
|
511
519
|
enc = Clacky::Utils::Encoding
|
|
520
|
+
command_name = extract_command_name(enc.to_utf8(result[:command].to_s))
|
|
521
|
+
|
|
522
|
+
# Apply truncate_and_save to all states including WAITING_INPUT and TIMEOUT
|
|
512
523
|
stdout = enc.to_utf8(result[:stdout] || "")
|
|
513
524
|
stderr = enc.to_utf8(result[:stderr] || "")
|
|
514
|
-
|
|
525
|
+
|
|
526
|
+
stdout_info = truncate_and_save(stdout, MAX_LLM_OUTPUT_CHARS, "stdout", command_name)
|
|
527
|
+
stderr_info = truncate_and_save(stderr, MAX_LLM_OUTPUT_CHARS, "stderr", command_name)
|
|
515
528
|
|
|
516
529
|
compact = {
|
|
517
530
|
command: enc.to_utf8(result[:command].to_s),
|
|
518
|
-
exit_code: exit_code,
|
|
519
|
-
success: result[:success]
|
|
531
|
+
exit_code: result[:exit_code] || 0,
|
|
532
|
+
success: result[:success],
|
|
533
|
+
stdout: stdout_info[:content],
|
|
534
|
+
stderr: stderr_info[:content]
|
|
520
535
|
}
|
|
521
536
|
|
|
522
|
-
compact[:elapsed] = result[:elapsed] if result[:elapsed]
|
|
523
|
-
|
|
524
|
-
command_name = extract_command_name(compact[:command])
|
|
525
|
-
|
|
526
|
-
stdout_info = truncate_and_save(stdout, MAX_LLM_OUTPUT_CHARS, "stdout", command_name)
|
|
527
|
-
compact[:stdout] = stdout_info[:content]
|
|
528
537
|
compact[:stdout_full] = stdout_info[:temp_file] if stdout_info[:temp_file]
|
|
529
|
-
|
|
530
|
-
stderr_info = truncate_and_save(stderr, MAX_LLM_OUTPUT_CHARS, "stderr", command_name)
|
|
531
|
-
compact[:stderr] = stderr_info[:content]
|
|
532
538
|
compact[:stderr_full] = stderr_info[:temp_file] if stderr_info[:temp_file]
|
|
533
|
-
|
|
534
539
|
compact[:output_truncated] = true if result[:output_truncated]
|
|
540
|
+
compact[:elapsed] = result[:elapsed] if result[:elapsed]
|
|
541
|
+
|
|
542
|
+
# Preserve WAITING_INPUT state fields
|
|
543
|
+
if result[:state] == 'WAITING_INPUT'
|
|
544
|
+
compact[:state] = 'WAITING_INPUT'
|
|
545
|
+
compact[:interaction_type] = result[:interaction_type]
|
|
546
|
+
compact[:message] = format_waiting_message(stdout_info[:content], result[:interaction_type] ? { type: result[:interaction_type], line: result.dig(:interaction, :line) || extract_last_line(stdout_info[:content]) } : { type: 'question', line: extract_last_line(stdout_info[:content]) })
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
# Preserve TIMEOUT state fields
|
|
550
|
+
if result[:state] == 'TIMEOUT'
|
|
551
|
+
compact[:state] = 'TIMEOUT'
|
|
552
|
+
compact[:timeout_type] = result[:timeout_type]
|
|
553
|
+
end
|
|
535
554
|
|
|
536
555
|
compact
|
|
537
556
|
end
|
|
@@ -398,6 +398,34 @@ module Clacky
|
|
|
398
398
|
# doesn't bleed into the next one, and so the buffer is ready before
|
|
399
399
|
# on_output starts firing (which can happen before show_progress is called).
|
|
400
400
|
@stdout_lines = nil
|
|
401
|
+
|
|
402
|
+
# Special handling for request_user_feedback: render as a readable interactive card
|
|
403
|
+
# with the full question and options, rather than the truncated format_call summary.
|
|
404
|
+
if name.to_s == "request_user_feedback"
|
|
405
|
+
args_data = args.is_a?(String) ? (JSON.parse(args, symbolize_names: true) rescue {}) : args
|
|
406
|
+
args_data = args_data.transform_keys(&:to_sym) if args_data.is_a?(Hash)
|
|
407
|
+
|
|
408
|
+
question = args_data[:question].to_s.strip
|
|
409
|
+
context = args_data[:context].to_s.strip
|
|
410
|
+
options = Array(args_data[:options])
|
|
411
|
+
|
|
412
|
+
theme = ThemeManager.current_theme
|
|
413
|
+
parts = []
|
|
414
|
+
|
|
415
|
+
parts << context unless context.empty?
|
|
416
|
+
parts << question unless question.empty?
|
|
417
|
+
|
|
418
|
+
if options.any?
|
|
419
|
+
parts << ""
|
|
420
|
+
options.each_with_index { |opt, i| parts << " #{i + 1}. #{opt}" }
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
card_text = parts.join("\n")
|
|
424
|
+
output = @renderer.render_system_message(card_text, prefix_newline: true)
|
|
425
|
+
append_output(output)
|
|
426
|
+
return
|
|
427
|
+
end
|
|
428
|
+
|
|
401
429
|
formatted_call = format_tool_call(name, args)
|
|
402
430
|
output = @renderer.render_tool_call(tool_name: name, formatted_call: formatted_call)
|
|
403
431
|
append_output(output)
|
|
@@ -466,7 +494,24 @@ module Clacky
|
|
|
466
494
|
# Show progress indicator with dynamic elapsed time
|
|
467
495
|
# @param message [String] Progress message (optional, will use random thinking verb if nil)
|
|
468
496
|
# @param prefix_newline [Boolean] Whether to add a blank line before progress (default: true)
|
|
497
|
+
# @param progress_type [String] "thinking" | "idle_compress" | "retrying" | custom
|
|
498
|
+
# @param phase [String] "active" (start spinner) | "done" (stop spinner, show final message)
|
|
499
|
+
# @param metadata [Hash] Extensible metadata (unused in UI2 for now)
|
|
469
500
|
def show_progress(message = nil, prefix_newline: true, progress_type: "thinking", phase: "active", metadata: {})
|
|
501
|
+
# phase: "done" → stop any active spinner and render the final message as a system message
|
|
502
|
+
if phase.to_s == "done"
|
|
503
|
+
stop_progress_thread
|
|
504
|
+
@stdout_lines = nil
|
|
505
|
+
@progress_start_time = nil
|
|
506
|
+
if message && !message.to_s.strip.empty?
|
|
507
|
+
output = @renderer.render_system_message(message.to_s, prefix_newline: false)
|
|
508
|
+
update_progress_line(output)
|
|
509
|
+
else
|
|
510
|
+
clear_progress_line
|
|
511
|
+
end
|
|
512
|
+
return
|
|
513
|
+
end
|
|
514
|
+
|
|
470
515
|
# Stop any existing progress thread
|
|
471
516
|
stop_progress_thread
|
|
472
517
|
|
|
@@ -609,14 +654,6 @@ module Clacky
|
|
|
609
654
|
append_output(output)
|
|
610
655
|
end
|
|
611
656
|
|
|
612
|
-
# Show idle compression status (two-phase: start → end).
|
|
613
|
-
# In terminal mode, only the final state is printed.
|
|
614
|
-
def show_idle_status(phase:, message:)
|
|
615
|
-
return unless phase.to_s == "end"
|
|
616
|
-
output = @renderer.render_system_message(message)
|
|
617
|
-
append_output(output)
|
|
618
|
-
end
|
|
619
|
-
|
|
620
657
|
# Show warning message
|
|
621
658
|
# @param message [String] Warning message
|
|
622
659
|
def show_warning(message)
|
|
@@ -113,7 +113,7 @@ module Clacky
|
|
|
113
113
|
def self.raise_helpful_error(call, tool_registry, original_error)
|
|
114
114
|
tool = tool_registry.get(call[:name])
|
|
115
115
|
error_msg = build_error_message(call, tool, original_error)
|
|
116
|
-
raise
|
|
116
|
+
raise BadArgumentsError, error_msg
|
|
117
117
|
end
|
|
118
118
|
|
|
119
119
|
def self.build_error_message(call, tool, original_error)
|
|
@@ -173,8 +173,13 @@ module Clacky
|
|
|
173
173
|
end
|
|
174
174
|
end
|
|
175
175
|
|
|
176
|
+
# Raised when tool call arguments are malformed or missing required params.
|
|
177
|
+
# Distinct from StandardError so the agent can handle it specially
|
|
178
|
+
# (e.g. retract the bad assistant message from history to break cache loops).
|
|
179
|
+
class BadArgumentsError < StandardError; end
|
|
180
|
+
|
|
176
181
|
# Custom exception for missing required parameters
|
|
177
|
-
class MissingRequiredParamsError <
|
|
182
|
+
class MissingRequiredParamsError < BadArgumentsError
|
|
178
183
|
attr_reader :tool_name, :missing_params, :provided_params
|
|
179
184
|
|
|
180
185
|
def initialize(tool_name, missing_params, provided_params)
|
data/lib/clacky/version.rb
CHANGED