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.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/.clacky/skills/gem-release/SKILL.md +13 -3
  3. data/CHANGELOG.md +40 -0
  4. data/README.md +11 -0
  5. data/lib/clacky/agent/hook_manager.rb +7 -3
  6. data/lib/clacky/agent/llm_caller.rb +33 -7
  7. data/lib/clacky/agent/message_compressor_helper.rb +13 -7
  8. data/lib/clacky/agent/session_serializer.rb +55 -4
  9. data/lib/clacky/agent.rb +214 -3
  10. data/lib/clacky/billing/platform_billing.rb +12 -2
  11. data/lib/clacky/cli.rb +12 -0
  12. data/lib/clacky/client.rb +36 -14
  13. data/lib/clacky/default_extensions/computer-use/ext.yml +15 -0
  14. data/lib/clacky/default_extensions/computer-use/lib/computer/geometry.rb +134 -0
  15. data/lib/clacky/default_extensions/computer-use/lib/computer/grid.rb +214 -0
  16. data/lib/clacky/default_extensions/computer-use/lib/computer/keycodes.rb +100 -0
  17. data/lib/clacky/default_extensions/computer-use/lib/computer/macos.rb +557 -0
  18. data/lib/clacky/default_extensions/computer-use/lib/computer/windows.rb +299 -0
  19. data/lib/clacky/default_extensions/computer-use/lib/computer/windows_agent.ps1 +436 -0
  20. data/lib/clacky/default_extensions/computer-use/lib/computer/windows_keycodes.rb +119 -0
  21. data/lib/clacky/default_extensions/computer-use/skills/computer-use/SKILL.md +141 -0
  22. data/lib/clacky/default_extensions/computer-use/skills/computer-use/bin/computer.rb +547 -0
  23. data/lib/clacky/extension/loader.rb +102 -25
  24. data/lib/clacky/extension/verifier.rb +1 -1
  25. data/lib/clacky/media/generator.rb +2 -2
  26. data/lib/clacky/message_format/open_ai.rb +28 -0
  27. data/lib/clacky/providers.rb +128 -110
  28. data/lib/clacky/server/browser_manager.rb +14 -0
  29. data/lib/clacky/server/channel/adapters/base.rb +19 -0
  30. data/lib/clacky/server/channel/adapters/feishu/adapter.rb +23 -1
  31. data/lib/clacky/server/channel/adapters/feishu/bot.rb +267 -0
  32. data/lib/clacky/server/channel/channel_manager.rb +10 -3
  33. data/lib/clacky/server/channel/channel_ui_controller.rb +224 -8
  34. data/lib/clacky/server/http_server.rb +124 -84
  35. data/lib/clacky/server/server_master.rb +17 -7
  36. data/lib/clacky/server/web_ui_controller.rb +2 -1
  37. data/lib/clacky/shell_hook_loader.rb +3 -3
  38. data/lib/clacky/skill.rb +3 -0
  39. data/lib/clacky/skill_loader.rb +5 -1
  40. data/lib/clacky/tools/ask_user.rb +1 -2
  41. data/lib/clacky/tools/file_reader.rb +9 -4
  42. data/lib/clacky/tools/terminal/session_manager.rb +3 -1
  43. data/lib/clacky/tools/terminal.rb +12 -1
  44. data/lib/clacky/utils/file_processor.rb +28 -3
  45. data/lib/clacky/utils/model_pricing.rb +95 -34
  46. data/lib/clacky/utils/resource_group.rb +67 -0
  47. data/lib/clacky/version.rb +1 -1
  48. data/lib/clacky/web/app.css +613 -455
  49. data/lib/clacky/web/app.js +113 -19
  50. data/lib/clacky/web/components/model-picker.js +174 -33
  51. data/lib/clacky/web/components/quote-select.js +51 -14
  52. data/lib/clacky/web/features/billing/view.js +60 -15
  53. data/lib/clacky/web/features/extensions/store.js +206 -272
  54. data/lib/clacky/web/features/extensions/view.js +383 -261
  55. data/lib/clacky/web/features/new-session/store.js +4 -2
  56. data/lib/clacky/web/features/new-session/view.js +81 -5
  57. data/lib/clacky/web/features/share/view.js +500 -571
  58. data/lib/clacky/web/features/workspace/view.js +8 -0
  59. data/lib/clacky/web/i18n.js +104 -38
  60. data/lib/clacky/web/index.html +25 -41
  61. data/lib/clacky/web/sessions.js +203 -89
  62. data/lib/clacky/web/settings.js +8 -0
  63. data/lib/clacky/web/skills.js +47 -16
  64. data/lib/clacky/web/ws-dispatcher.js +18 -6
  65. data/lib/clacky.rb +13 -1
  66. metadata +12 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 878e99c7061a59dee8c50f4ec416986c33c7d93538595e90719042827a9b774e
4
- data.tar.gz: 51a940cdc2e2f25b95a6ce96d84cd7c45988154b324ca2c9962bcddf4f213fa4
3
+ metadata.gz: 9939ab09ac4e473dc7b096dac6a14c67f6d6060fe7678d7bd0b2635c7cc924ed
4
+ data.tar.gz: 9e0e566a77267f73388a9761d101672c86fd35513dc7248355a3c3df7d8017f6
5
5
  SHA512:
6
- metadata.gz: 82af0b7e1e966e3e8eafc888006a05e75f637b6d6508721432bc484394f9699cb2a82ad0f0abee73cd88214bcad6e865acf8e1840e357059a470aee5e59f00c5
7
- data.tar.gz: f996cf6c38e1070608dd49955e68c1154301152b8fc51d0e3e392055dcd51bebc21c234e0bf20fddbdb0a2a9ba48dcc527afcfd1e92e43fc2fc82a1ddbbcfa33
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 -oP '#\d+' | while read pr; do
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
- - If a PR was closed and resubmitted, use the merged PR number
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 call itself.
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 — the hook has already produced
47
- # the tool's result, so later hooks have nothing left to act on.
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, "[LLM] Model returned empty response (no content, no tool_calls), retrying..."
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, "[LLM] Thinking-mode model produced reasoning but empty content/tool_calls, retrying..."
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
- token_data = track_cost(response[:usage], raw_api_usage: response[:raw_api_usage], model: api_call_model)
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 only the lightweight UI metadata needed to reconstruct file
682
- # badges after compression. File paths, previews, sizes, MIME types, and
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
- files << { name: name, type: "image" }
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
- end.uniq { |file| [file[:name], file[:type]] }
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
- # Only name + type are accepted; paths and file contents are never restored.
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
- files << { name: name.to_s, type: type.to_s }
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
- ui.show_tool_result(blk[:content].to_s)
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.show_tool_result(msg[:content].to_s)
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.