openclacky 1.5.15 → 1.5.16
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/gem-release/SKILL.md +13 -3
- data/CHANGELOG.md +40 -0
- data/README.md +11 -0
- data/lib/clacky/agent/hook_manager.rb +7 -3
- data/lib/clacky/agent/llm_caller.rb +33 -7
- data/lib/clacky/agent/message_compressor_helper.rb +13 -7
- data/lib/clacky/agent/session_serializer.rb +55 -4
- data/lib/clacky/agent.rb +214 -3
- data/lib/clacky/billing/platform_billing.rb +12 -2
- data/lib/clacky/cli.rb +12 -0
- data/lib/clacky/client.rb +36 -14
- data/lib/clacky/default_extensions/computer-use/ext.yml +15 -0
- data/lib/clacky/default_extensions/computer-use/lib/computer/geometry.rb +134 -0
- data/lib/clacky/default_extensions/computer-use/lib/computer/grid.rb +214 -0
- data/lib/clacky/default_extensions/computer-use/lib/computer/keycodes.rb +100 -0
- data/lib/clacky/default_extensions/computer-use/lib/computer/macos.rb +557 -0
- data/lib/clacky/default_extensions/computer-use/lib/computer/windows.rb +299 -0
- data/lib/clacky/default_extensions/computer-use/lib/computer/windows_agent.ps1 +436 -0
- data/lib/clacky/default_extensions/computer-use/lib/computer/windows_keycodes.rb +119 -0
- data/lib/clacky/default_extensions/computer-use/skills/computer-use/SKILL.md +141 -0
- data/lib/clacky/default_extensions/computer-use/skills/computer-use/bin/computer.rb +547 -0
- data/lib/clacky/extension/loader.rb +102 -25
- data/lib/clacky/extension/verifier.rb +1 -1
- data/lib/clacky/media/generator.rb +2 -2
- data/lib/clacky/message_format/open_ai.rb +28 -0
- data/lib/clacky/providers.rb +128 -110
- data/lib/clacky/server/browser_manager.rb +14 -0
- data/lib/clacky/server/channel/adapters/base.rb +19 -0
- data/lib/clacky/server/channel/adapters/feishu/adapter.rb +23 -1
- data/lib/clacky/server/channel/adapters/feishu/bot.rb +267 -0
- data/lib/clacky/server/channel/channel_manager.rb +10 -3
- data/lib/clacky/server/channel/channel_ui_controller.rb +224 -8
- data/lib/clacky/server/http_server.rb +124 -84
- data/lib/clacky/server/server_master.rb +17 -7
- data/lib/clacky/server/web_ui_controller.rb +2 -1
- data/lib/clacky/shell_hook_loader.rb +3 -3
- data/lib/clacky/skill.rb +3 -0
- data/lib/clacky/skill_loader.rb +5 -1
- data/lib/clacky/tools/ask_user.rb +1 -2
- data/lib/clacky/tools/file_reader.rb +9 -4
- data/lib/clacky/tools/terminal/session_manager.rb +3 -1
- data/lib/clacky/tools/terminal.rb +12 -1
- data/lib/clacky/utils/file_processor.rb +28 -3
- data/lib/clacky/utils/model_pricing.rb +95 -34
- data/lib/clacky/utils/resource_group.rb +67 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +613 -455
- data/lib/clacky/web/app.js +113 -19
- data/lib/clacky/web/components/model-picker.js +174 -33
- data/lib/clacky/web/components/quote-select.js +51 -14
- data/lib/clacky/web/features/billing/view.js +60 -15
- data/lib/clacky/web/features/extensions/store.js +206 -272
- data/lib/clacky/web/features/extensions/view.js +383 -261
- data/lib/clacky/web/features/new-session/store.js +4 -2
- data/lib/clacky/web/features/new-session/view.js +81 -5
- data/lib/clacky/web/features/share/view.js +500 -571
- data/lib/clacky/web/features/workspace/view.js +8 -0
- data/lib/clacky/web/i18n.js +104 -38
- data/lib/clacky/web/index.html +25 -41
- data/lib/clacky/web/sessions.js +203 -89
- data/lib/clacky/web/settings.js +8 -0
- data/lib/clacky/web/skills.js +47 -16
- data/lib/clacky/web/ws-dispatcher.js +18 -6
- data/lib/clacky.rb +13 -1
- metadata +12 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9939ab09ac4e473dc7b096dac6a14c67f6d6060fe7678d7bd0b2635c7cc924ed
|
|
4
|
+
data.tar.gz: 9e0e566a77267f73388a9761d101672c86fd35513dc7248355a3c3df7d8017f6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 206657c6e08b40482a5029abf25fb8bd8579728ea469e2b2f7fba5b0f95dbbbcbfb8ca75778f73090bbbb34d7145cf58dab64c7d7462325d981a4e5106ced23e
|
|
7
|
+
data.tar.gz: 0b30e40beb4d544076f2063160d04d9fcc2099b43ce46c78e1694d35ec631859361ffc15b2521a8a53e0c0cd84fb52e157cebf4469557991f273770842808568
|
|
@@ -95,15 +95,25 @@ This is the one step the agent handles manually — the script does not write ch
|
|
|
95
95
|
```bash
|
|
96
96
|
gh api orgs/clacky-ai/members --paginate --jq '.[].login' | sort
|
|
97
97
|
```
|
|
98
|
-
- Then cross-reference PR authors in the release range
|
|
98
|
+
- Then cross-reference PR authors in the release range. Use `grep -oE`, not `-oP` —
|
|
99
|
+
BSD grep on macOS rejects `-P` with "invalid option", and the loop silently yields nothing:
|
|
99
100
|
```bash
|
|
100
|
-
git log <previous_tag>..HEAD --oneline | grep -
|
|
101
|
+
git log <previous_tag>..HEAD --oneline | grep -oE '#[0-9]+' | while read pr; do
|
|
101
102
|
num=${pr#\#}
|
|
102
103
|
gh pr view $num --json number,author --jq '"#\(.number) \(.author.login)"'
|
|
103
104
|
done
|
|
104
105
|
```
|
|
105
106
|
- Skip attribution for org members (internal contributors) - only call out external contributors
|
|
106
|
-
-
|
|
107
|
+
- Some cited numbers no longer resolve at all. A commit subject can name a superseded PR
|
|
108
|
+
next to the merged one (`feat: discover custom-endpoint models in the add-model dialog
|
|
109
|
+
(#559) (#560)`), and `gh pr view 559` answers "Could not resolve to a PullRequest".
|
|
110
|
+
When that happens, ask the commit which PR actually merged it:
|
|
111
|
+
```bash
|
|
112
|
+
gh api repos/clacky-ai/openclacky/commits/<sha>/pulls \
|
|
113
|
+
--jq '.[] | "PR #\(.number) | \(.user.login) | \(.state)"'
|
|
114
|
+
```
|
|
115
|
+
Attribute the bullet to the PR that resolves and carries a real author; a number that
|
|
116
|
+
resolves to nobody is not a contributor reference.
|
|
107
117
|
- Example: `- Todo panel with SVG icons (#437 - @shipinliang)`
|
|
108
118
|
|
|
109
119
|
6. Commit the changelog:
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,46 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
|
|
7
|
+
## [1.5.16] - 2026-09-24
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- Add Claude Opus 5.5 and GPT-6 Sol/Luna to the model catalog
|
|
11
|
+
- Send task progress cards in Feishu, with collapsible process history (#582)
|
|
12
|
+
- Print a receipt-style usage scorecard from the billing page and share it as an image
|
|
13
|
+
- Understand video attachments through a sidecar model (#564)
|
|
14
|
+
- Open the app straight into a chat from an external clacky:// link, including the welcome view
|
|
15
|
+
- Route requests automatically: the auto alias picks a tier per request and reports which tier served it (#576)
|
|
16
|
+
- Drive the Windows desktop through WSL in computer-use
|
|
17
|
+
- Ship computer-use as a default-off extension
|
|
18
|
+
- Let on_start hooks veto a turn before the agent starts (#573 - @afumu)
|
|
19
|
+
- Cap the memory and CPU of a task's shells and browser daemons on Linux hosts with --task-cgroup
|
|
20
|
+
- Refuse to start on a taken port instead of silently moving to another one, with --strict-port
|
|
21
|
+
- Keep attachment paths when a compressed history chunk is replayed (#581 - @BoyYangzai)
|
|
22
|
+
- Point generated skills at their own directory through an inlined skill_dir
|
|
23
|
+
|
|
24
|
+
### Improved
|
|
25
|
+
- Restyle the web search result card (#575)
|
|
26
|
+
- Use official brand marks for model vendor badges (#566)
|
|
27
|
+
- Refresh the supported model catalogs and pricing (#571)
|
|
28
|
+
- Rename the extension group label User to Personal
|
|
29
|
+
- Adjust the session title size and unify the sidebar active highlight
|
|
30
|
+
- Soften the extension row action buttons and tighten the segmented control corners
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
- Keep queued guidance attached to the message it was sent from, and allow cancelling it (#584, #578)
|
|
34
|
+
- Stop treating a pasted path as a slash command (#577)
|
|
35
|
+
- Restore structured tool result cards when a session history is replayed (#574)
|
|
36
|
+
- Stop duplicating user bubbles after an edit (#568)
|
|
37
|
+
- Show attachment badges in realtime user bubbles (#567)
|
|
38
|
+
- Transcribe audio attachments through the STT sidecar (#565)
|
|
39
|
+
- Preserve a quote selection while a live update streams in (#569)
|
|
40
|
+
- Load per-key platform usage in parallel in the billing view (#580)
|
|
41
|
+
- Prevent subagents from publishing input queues (#570 - @afumu)
|
|
42
|
+
- Block model switching while a session is running instead of racing the running turn
|
|
43
|
+
- Hand binary files back to the OS when they are reopened
|
|
44
|
+
- Report the doctor host before resolving the backend
|
|
45
|
+
- Stop the DeepSeek preset from showing a version suffix
|
|
46
|
+
|
|
7
47
|
## [1.5.15] - 2026-09-17
|
|
8
48
|
|
|
9
49
|
### Added
|
data/README.md
CHANGED
|
@@ -185,6 +185,17 @@ openclacky server --port 8080 # custom port
|
|
|
185
185
|
openclacky server --host 0.0.0.0 # listen on all interfaces (remote access)
|
|
186
186
|
```
|
|
187
187
|
|
|
188
|
+
Linux sandbox hosts can opt into task limits with
|
|
189
|
+
`openclacky server --task-cgroup /sys/fs/cgroup/tasks`. The host must first create
|
|
190
|
+
that cgroup, configure its aggregate memory/CPU limits, and enable the `memory`
|
|
191
|
+
and `cpu` subtree controllers. Terminal shells and browser MCP daemons enter
|
|
192
|
+
child groups before exec; the server remains outside. Terminal results include
|
|
193
|
+
`resource_exhausted: "memory"` and an error when their group records an OOM kill,
|
|
194
|
+
even if the shell masks the child's failure. A separately managed Chrome must
|
|
195
|
+
also be placed under the same parent by its launcher. This option is off by
|
|
196
|
+
default and does not establish a security boundary against privileged commands.
|
|
197
|
+
|
|
198
|
+
|
|
188
199
|
## Configuration
|
|
189
200
|
|
|
190
201
|
```bash
|
|
@@ -25,7 +25,11 @@ module Clacky
|
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
# @return [Hash] `{action: :allow}`, `{action: :deny, reason:}`, or
|
|
28
|
-
# `{action: :handled, result:}` when a hook fulfilled the
|
|
28
|
+
# `{action: :handled, result:}` when a hook fulfilled the event itself.
|
|
29
|
+
# on_start receives (user_input, agent) after task/history initialization.
|
|
30
|
+
# Its handled result is returned unchanged from Agent#run; the extension
|
|
31
|
+
# supplies the run-result status and any queue/feedback flags it needs.
|
|
32
|
+
# on_start is not fired for steering input inside an already running turn.
|
|
29
33
|
# Extra trailing arg: the agent that owns this hook chain, so ext hooks can
|
|
30
34
|
# call `agent.emit_event(...)`. Blocks are procs — those declaring fewer
|
|
31
35
|
# params (`|call|`, `|call, result|`) silently ignore it.
|
|
@@ -43,8 +47,8 @@ module Clacky
|
|
|
43
47
|
# place (chained rewrite), so for non-deny results there's nothing to
|
|
44
48
|
# merge — we just keep going.
|
|
45
49
|
#
|
|
46
|
-
# :handled short-circuits the same way
|
|
47
|
-
# the
|
|
50
|
+
# :handled short-circuits the same way: the hook has already fulfilled
|
|
51
|
+
# the event, so later hooks have nothing left to act on.
|
|
48
52
|
if hook_result[:action] == :deny || hook_result[:action] == :handled
|
|
49
53
|
result = hook_result
|
|
50
54
|
break
|
|
@@ -45,7 +45,7 @@ module Clacky
|
|
|
45
45
|
# Inside call_llm we only *update in place* during retries, so the
|
|
46
46
|
# already-live progress slot shows meaningful transient status
|
|
47
47
|
# ("Network failed… attempt 2/10", etc.).
|
|
48
|
-
private def call_llm
|
|
48
|
+
private def call_llm(agent_upstream_fails: nil, agent_upgrade_fails: nil)
|
|
49
49
|
# Transition :fallback_active → :probing if cooling-off has expired.
|
|
50
50
|
@config.maybe_start_probing
|
|
51
51
|
|
|
@@ -54,6 +54,12 @@ module Clacky
|
|
|
54
54
|
max_retries = 10
|
|
55
55
|
retry_delay = 5
|
|
56
56
|
retries = 0
|
|
57
|
+
# Per-call tier attribution for auto routing: each retryable failure is
|
|
58
|
+
# charged to the tier the gateway actually routed that call to (echoed
|
|
59
|
+
# in X-Clacky-Routed-Tier). Failures without a tier (network layer,
|
|
60
|
+
# timeouts — the gateway never answered) are not attributed to either.
|
|
61
|
+
floor_fails = 0
|
|
62
|
+
upgrade_fails = 0
|
|
57
63
|
|
|
58
64
|
# Track whether any of the retry/fallback branches below opened a
|
|
59
65
|
# "retrying" progress slot via show_progress(progress_type:
|
|
@@ -111,7 +117,9 @@ module Clacky
|
|
|
111
117
|
max_tokens: @config.max_tokens,
|
|
112
118
|
enable_caching: @config.enable_prompt_caching,
|
|
113
119
|
reasoning_effort: @reasoning_effort,
|
|
114
|
-
on_chunk: build_progress_on_chunk
|
|
120
|
+
on_chunk: build_progress_on_chunk,
|
|
121
|
+
agent_upstream_fails: (agent_upstream_fails || 0) + floor_fails,
|
|
122
|
+
agent_upgrade_fails: (agent_upgrade_fails || 0) + upgrade_fails
|
|
115
123
|
)
|
|
116
124
|
|
|
117
125
|
# Successful response — if we were probing, confirm primary is healthy.
|
|
@@ -142,7 +150,7 @@ module Clacky
|
|
|
142
150
|
finish_reason: response[:finish_reason].to_s,
|
|
143
151
|
completion_tokens: response.dig(:token_usage, :completion_tokens)
|
|
144
152
|
)
|
|
145
|
-
raise RetryableError
|
|
153
|
+
raise RetryableError.new("[LLM] Model returned empty response (no content, no tool_calls), retrying...", routed_tier: response[:routed_tier])
|
|
146
154
|
end
|
|
147
155
|
|
|
148
156
|
# Thinking-mode silent response detector. DeepSeek V4 / Kimi K2 /
|
|
@@ -164,7 +172,7 @@ module Clacky
|
|
|
164
172
|
reasoning_tail: reasoning_str[-200, 200] || reasoning_str,
|
|
165
173
|
completion_tokens: response.dig(:token_usage, :completion_tokens)
|
|
166
174
|
)
|
|
167
|
-
raise RetryableError
|
|
175
|
+
raise RetryableError.new("[LLM] Thinking-mode model produced reasoning but empty content/tool_calls, retrying...", routed_tier: response[:routed_tier])
|
|
168
176
|
end
|
|
169
177
|
|
|
170
178
|
rescue Faraday::TimeoutError => e
|
|
@@ -275,6 +283,11 @@ module Clacky
|
|
|
275
283
|
Clacky::Shutdown.checkpoint!
|
|
276
284
|
retries += 1
|
|
277
285
|
|
|
286
|
+
case e.routed_tier
|
|
287
|
+
when "floor" then floor_fails += 1
|
|
288
|
+
when "upgrade" then upgrade_fails += 1
|
|
289
|
+
end
|
|
290
|
+
|
|
278
291
|
# Probing failure: primary still down — renew cooling-off and retry with fallback.
|
|
279
292
|
if @config.probing?
|
|
280
293
|
handle_probe_failure
|
|
@@ -398,10 +411,21 @@ module Clacky
|
|
|
398
411
|
raise
|
|
399
412
|
end
|
|
400
413
|
|
|
414
|
+
# Fold this call's tier-attributed failures into the task-level tallies
|
|
415
|
+
# so later calls in the same task keep routing away from the failing
|
|
416
|
+
# lane — the per-call counters above reset to 0 once this call
|
|
417
|
+
# succeeds. A floor failure proves the floor lane is shaky for this
|
|
418
|
+
# task (route up); an upgrade failure proves the upgrade lane is shaky
|
|
419
|
+
# (route back down and stay on the floor until the task boundary).
|
|
420
|
+
@task_upstream_fails = (@task_upstream_fails || 0) + floor_fails if floor_fails > 0
|
|
421
|
+
@task_upgrade_fails = (@task_upgrade_fails || 0) + upgrade_fails if upgrade_fails > 0
|
|
422
|
+
|
|
401
423
|
# Track cost and collect token usage data.
|
|
402
424
|
# Pass the model name captured at API call time to ensure accurate billing
|
|
403
425
|
# even if the user switched models during the (potentially long) API call.
|
|
404
|
-
|
|
426
|
+
# For the "auto" alias the gateway reports the concrete model it routed
|
|
427
|
+
# to via X-Clacky-Routed-Model — bill under that so pricing resolves.
|
|
428
|
+
token_data = track_cost(response[:usage], raw_api_usage: response[:raw_api_usage], model: response[:routed_model] || api_call_model)
|
|
405
429
|
response[:token_usage] = token_data
|
|
406
430
|
|
|
407
431
|
# [DIAG] Log raw client response shape. Only emit when we see the
|
|
@@ -721,9 +745,11 @@ module Clacky
|
|
|
721
745
|
# and the hint just sits in history without affecting behaviour.
|
|
722
746
|
inject_upstream_truncation_hint_if_first(truncated)
|
|
723
747
|
|
|
724
|
-
raise Clacky::UpstreamTruncatedError
|
|
748
|
+
raise Clacky::UpstreamTruncatedError.new(
|
|
725
749
|
"[LLM] Upstream truncated tool_call `#{truncated[:name]}` " \
|
|
726
|
-
"(args=#{args_str[0, 40].inspect}). Retrying..."
|
|
750
|
+
"(args=#{args_str[0, 40].inspect}). Retrying...",
|
|
751
|
+
routed_tier: response[:routed_tier]
|
|
752
|
+
)
|
|
727
753
|
end
|
|
728
754
|
|
|
729
755
|
# True when a tool_call's arguments field is unusable — either empty
|
|
@@ -678,9 +678,8 @@ module Clacky
|
|
|
678
678
|
lines
|
|
679
679
|
end
|
|
680
680
|
|
|
681
|
-
# Serialize
|
|
682
|
-
#
|
|
683
|
-
# contents deliberately stay out of the chunk archive.
|
|
681
|
+
# Serialize lightweight UI metadata for file badges after compression.
|
|
682
|
+
# Includes path/preview_path when set; file contents and sizes stay out of the archive.
|
|
684
683
|
def display_files_for_archive(msg)
|
|
685
684
|
files = Array(msg[:display_files]).dup
|
|
686
685
|
Array(msg[:content]).each do |block|
|
|
@@ -688,12 +687,14 @@ module Clacky
|
|
|
688
687
|
next unless %w[image image_url].include?((block[:type] || block["type"]).to_s)
|
|
689
688
|
|
|
690
689
|
name = block[:image_name] || block["image_name"]
|
|
690
|
+
path = block[:image_path] || block["image_path"]
|
|
691
691
|
if name.nil? || name.to_s.strip.empty?
|
|
692
|
-
path = block[:image_path] || block["image_path"]
|
|
693
692
|
name = File.basename(path.to_s).sub(/\A[0-9a-f]{16}_/, "") unless path.to_s.empty?
|
|
694
693
|
end
|
|
695
694
|
name = "image" if name.nil? || name.to_s.strip.empty?
|
|
696
|
-
|
|
695
|
+
entry = { name: name, type: "image" }
|
|
696
|
+
entry[:path] = path.to_s unless path.nil? || path.to_s.strip.empty?
|
|
697
|
+
files << entry
|
|
697
698
|
end
|
|
698
699
|
|
|
699
700
|
files.filter_map do |file|
|
|
@@ -704,8 +705,13 @@ module Clacky
|
|
|
704
705
|
|
|
705
706
|
type = file[:type] || file["type"] || "file"
|
|
706
707
|
type = "file" if type.to_s.strip.empty?
|
|
707
|
-
{ name: name.to_s, type: type.to_s }
|
|
708
|
-
|
|
708
|
+
entry = { name: name.to_s, type: type.to_s }
|
|
709
|
+
path = file[:path] || file["path"]
|
|
710
|
+
entry[:path] = path.to_s unless path.nil? || path.to_s.strip.empty?
|
|
711
|
+
preview = file[:preview_path] || file["preview_path"]
|
|
712
|
+
entry[:preview_path] = preview.to_s unless preview.nil? || preview.to_s.strip.empty?
|
|
713
|
+
entry
|
|
714
|
+
end.uniq { |file| [file[:name], file[:type], file[:path]] }
|
|
709
715
|
end
|
|
710
716
|
|
|
711
717
|
# Once an image has a badge in the archive, omit the generic [image_url]
|
|
@@ -576,7 +576,7 @@ module Clacky
|
|
|
576
576
|
|
|
577
577
|
# Pull lightweight attachment badge metadata out of a chunk section.
|
|
578
578
|
# The marker is internal archive data and must not appear in replayed text.
|
|
579
|
-
#
|
|
579
|
+
# Restores name, type, and path/preview_path when present in the chunk marker.
|
|
580
580
|
def extract_display_files_from_text(text)
|
|
581
581
|
return [text, []] unless text.include?("_Display files:")
|
|
582
582
|
|
|
@@ -594,7 +594,12 @@ module Clacky
|
|
|
594
594
|
|
|
595
595
|
type = file["type"] || file[:type] || "file"
|
|
596
596
|
type = "file" if type.to_s.strip.empty?
|
|
597
|
-
|
|
597
|
+
entry = { name: name.to_s, type: type.to_s }
|
|
598
|
+
path = file["path"] || file[:path]
|
|
599
|
+
entry[:path] = path.to_s unless path.nil? || path.to_s.strip.empty?
|
|
600
|
+
preview = file["preview_path"] || file[:preview_path]
|
|
601
|
+
entry[:preview_path] = preview.to_s unless preview.nil? || preview.to_s.strip.empty?
|
|
602
|
+
files << entry
|
|
598
603
|
end
|
|
599
604
|
true
|
|
600
605
|
end
|
|
@@ -750,6 +755,7 @@ module Clacky
|
|
|
750
755
|
questions: questions)
|
|
751
756
|
end
|
|
752
757
|
else
|
|
758
|
+
remember_replayed_tool_name(tc[:id], name)
|
|
753
759
|
ui.show_tool_call(name, args)
|
|
754
760
|
end
|
|
755
761
|
end
|
|
@@ -768,20 +774,65 @@ module Clacky
|
|
|
768
774
|
msg[:content].each do |blk|
|
|
769
775
|
next unless blk.is_a?(Hash) && blk[:type] == "tool_result"
|
|
770
776
|
|
|
771
|
-
|
|
777
|
+
content = blk[:content].to_s
|
|
778
|
+
emit_replayed_tool_result(ui, content, blk[:tool_use_id])
|
|
772
779
|
end
|
|
773
780
|
replay_subagent_transcript(msg, ui)
|
|
774
781
|
end
|
|
775
782
|
|
|
776
783
|
when "tool"
|
|
777
784
|
# OpenAI-format tool result
|
|
778
|
-
ui
|
|
785
|
+
emit_replayed_tool_result(ui, msg[:content].to_s, msg[:tool_call_id])
|
|
779
786
|
replay_subagent_transcript(msg, ui)
|
|
780
787
|
end
|
|
781
788
|
|
|
782
789
|
replay_ext_events(msg, ui)
|
|
783
790
|
end
|
|
784
791
|
|
|
792
|
+
# Tool results persist as the raw JSON the tool returned, so replay can
|
|
793
|
+
# rebuild the same structured `ui:` payload the live path sent instead of
|
|
794
|
+
# degrading the card to plain text (C-5797).
|
|
795
|
+
private def emit_replayed_tool_result(ui, content, tool_call_id)
|
|
796
|
+
ui_payload = replayed_tool_ui(content, tool_call_id)
|
|
797
|
+
if ui_payload
|
|
798
|
+
ui.show_tool_result(content, ui: ui_payload)
|
|
799
|
+
else
|
|
800
|
+
ui.show_tool_result(content)
|
|
801
|
+
end
|
|
802
|
+
end
|
|
803
|
+
|
|
804
|
+
# Tool names live on the preceding assistant message, so remember them by
|
|
805
|
+
# call id while walking the round.
|
|
806
|
+
private def remember_replayed_tool_name(call_id, name)
|
|
807
|
+
return if call_id.nil? || name.to_s.empty?
|
|
808
|
+
|
|
809
|
+
@replayed_tool_names ||= {}
|
|
810
|
+
@replayed_tool_names[call_id.to_s] = name
|
|
811
|
+
end
|
|
812
|
+
|
|
813
|
+
private def replayed_tool_ui(content, tool_call_id)
|
|
814
|
+
return nil if tool_call_id.nil?
|
|
815
|
+
|
|
816
|
+
name = (@replayed_tool_names || {})[tool_call_id.to_s]
|
|
817
|
+
return nil if name.nil?
|
|
818
|
+
|
|
819
|
+
text = content.to_s
|
|
820
|
+
return nil unless text.start_with?("{")
|
|
821
|
+
|
|
822
|
+
resolved = @tool_registry.resolve(name)
|
|
823
|
+
return nil if resolved.nil?
|
|
824
|
+
|
|
825
|
+
tool = @tool_registry.get(resolved)
|
|
826
|
+
return nil unless tool.respond_to?(:ui_result)
|
|
827
|
+
|
|
828
|
+
parsed = JSON.parse(text, symbolize_names: true)
|
|
829
|
+
return nil unless parsed.is_a?(Hash)
|
|
830
|
+
|
|
831
|
+
tool.ui_result(parsed)
|
|
832
|
+
rescue StandardError
|
|
833
|
+
nil
|
|
834
|
+
end
|
|
835
|
+
|
|
785
836
|
# Re-emit custom extension events recorded on a message so extension
|
|
786
837
|
# panels can rebuild their state after a page reload. Mirrors what
|
|
787
838
|
# Agent#emit_event pushed live. No-op when the message carries none.
|