openclacky 1.5.10 → 1.5.12

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 (110) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +60 -0
  3. data/README.md +4 -4
  4. data/README_CN.md +4 -4
  5. data/README_JA.md +4 -4
  6. data/lib/clacky/agent/cost_tracker.rb +2 -0
  7. data/lib/clacky/agent/llm_caller.rb +7 -3
  8. data/lib/clacky/agent/message_compressor_helper.rb +1 -1
  9. data/lib/clacky/agent/session_serializer.rb +60 -30
  10. data/lib/clacky/agent/skill_manager.rb +1 -2
  11. data/lib/clacky/agent/time_machine.rb +1 -0
  12. data/lib/clacky/agent/tool_executor.rb +1 -1
  13. data/lib/clacky/agent/tool_registry.rb +6 -4
  14. data/lib/clacky/agent.rb +197 -47
  15. data/lib/clacky/billing/billing_record.rb +3 -0
  16. data/lib/clacky/billing/billing_store.rb +31 -5
  17. data/lib/clacky/billing/platform_billing.rb +148 -0
  18. data/lib/clacky/brand_config.rb +15 -17
  19. data/lib/clacky/cancel_flag.rb +23 -0
  20. data/lib/clacky/cli.rb +2 -2
  21. data/lib/clacky/client.rb +5 -2
  22. data/lib/clacky/default_extensions/advisor/advisors/general.md +54 -0
  23. data/lib/clacky/default_extensions/advisor/api/handler.rb +27 -0
  24. data/lib/clacky/default_extensions/advisor/ext.yml +35 -0
  25. data/lib/clacky/default_extensions/advisor/hooks/advisor.rb +390 -0
  26. data/lib/clacky/default_extensions/advisor/hooks/advisor_complete.rb +11 -0
  27. data/lib/clacky/default_extensions/advisor/hooks/advisor_tool_use.rb +12 -0
  28. data/lib/clacky/default_extensions/advisor/panels/advisor/view.js +479 -0
  29. data/lib/clacky/default_extensions/ext-studio/agents/ext-developer/system_prompt.md +6 -19
  30. data/lib/clacky/default_extensions/ext-studio/api/handler.rb +29 -0
  31. data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +125 -59
  32. data/lib/clacky/default_extensions/ext-studio/skills/ext-develop/SKILL.md +25 -39
  33. data/lib/clacky/default_extensions/git/panels/git/view.js +10 -0
  34. data/lib/clacky/default_extensions/meeting/api/handler.rb +1 -1
  35. data/lib/clacky/default_skills/channel-manager/SKILL.md +4 -4
  36. data/lib/clacky/default_skills/new/SKILL.md +1 -1
  37. data/lib/clacky/default_skills/onboard/SKILL.md +2 -2
  38. data/lib/clacky/extension/hook_loader.rb +2 -0
  39. data/lib/clacky/extension/packager.rb +23 -10
  40. data/lib/clacky/extension/scaffold/templates/full/panels/dashboard/view.js.erb +12 -25
  41. data/lib/clacky/extension/scaffold/templates/hello/panels/hello/view.js.erb +3 -3
  42. data/lib/clacky/fanout.rb +50 -3
  43. data/lib/clacky/idle_compression_timer.rb +3 -5
  44. data/lib/clacky/json_ui_controller.rb +3 -2
  45. data/lib/clacky/mcp/http_transport.rb +1 -1
  46. data/lib/clacky/mcp/registry.rb +2 -3
  47. data/lib/clacky/mcp/stdio_transport.rb +21 -17
  48. data/lib/clacky/message_history.rb +38 -0
  49. data/lib/clacky/plain_ui_controller.rb +30 -17
  50. data/lib/clacky/platform_http_client.rb +14 -14
  51. data/lib/clacky/providers.rb +23 -10
  52. data/lib/clacky/rich_ui/rich_ui_controller.rb +6 -6
  53. data/lib/clacky/server/browser_manager.rb +4 -7
  54. data/lib/clacky/server/channel/adapters/dingtalk/stream_client.rb +2 -1
  55. data/lib/clacky/server/channel/adapters/discord/gateway_client.rb +7 -5
  56. data/lib/clacky/server/channel/adapters/feishu/ws_client.rb +4 -3
  57. data/lib/clacky/server/channel/adapters/telegram/adapter.rb +3 -2
  58. data/lib/clacky/server/channel/adapters/telegram/api_client.rb +5 -3
  59. data/lib/clacky/server/channel/adapters/wecom/ws_client.rb +6 -5
  60. data/lib/clacky/server/channel/adapters/weixin/adapter.rb +13 -11
  61. data/lib/clacky/server/channel/adapters/weixin/api_client.rb +7 -2
  62. data/lib/clacky/server/channel/channel_config.rb +25 -9
  63. data/lib/clacky/server/channel/channel_manager.rb +109 -27
  64. data/lib/clacky/server/channel/channel_ui_controller.rb +40 -8
  65. data/lib/clacky/server/http_server.rb +423 -71
  66. data/lib/clacky/server/scheduler.rb +4 -5
  67. data/lib/clacky/server/server_master.rb +15 -3
  68. data/lib/clacky/server/session_registry.rb +7 -2
  69. data/lib/clacky/server/web_ui_controller.rb +19 -18
  70. data/lib/clacky/session_manager.rb +82 -18
  71. data/lib/clacky/shell_hook_loader.rb +3 -3
  72. data/lib/clacky/shutdown.rb +71 -0
  73. data/lib/clacky/telemetry.rb +1 -1
  74. data/lib/clacky/thread_registry.rb +102 -0
  75. data/lib/clacky/tools/ask_user.rb +213 -0
  76. data/lib/clacky/tools/base.rb +5 -0
  77. data/lib/clacky/tools/terminal.rb +4 -1
  78. data/lib/clacky/tools/trash_manager.rb +16 -0
  79. data/lib/clacky/tools/web_search.rb +1 -1
  80. data/lib/clacky/ui2/components/input_area.rb +2 -2
  81. data/lib/clacky/ui2/progress_handle.rb +1 -2
  82. data/lib/clacky/ui2/progress_indicator.rb +1 -1
  83. data/lib/clacky/ui2/ui_controller.rb +23 -19
  84. data/lib/clacky/ui_interface.rb +4 -4
  85. data/lib/clacky/utils/arguments_parser.rb +31 -4
  86. data/lib/clacky/utils/model_pricing.rb +59 -0
  87. data/lib/clacky/utils/parser_manager.rb +2 -2
  88. data/lib/clacky/version.rb +1 -1
  89. data/lib/clacky/web/app.css +750 -115
  90. data/lib/clacky/web/components/code-editor.js +33 -1
  91. data/lib/clacky/web/components/composer.js +277 -0
  92. data/lib/clacky/web/components/mentions.js +599 -0
  93. data/lib/clacky/web/components/notify.js +9 -7
  94. data/lib/clacky/web/core/aside.js +3 -3
  95. data/lib/clacky/web/core/ext.js +4 -6
  96. data/lib/clacky/web/features/billing/store.js +2 -6
  97. data/lib/clacky/web/features/billing/view.js +12 -10
  98. data/lib/clacky/web/features/channels/store.js +16 -1
  99. data/lib/clacky/web/features/channels/view.js +45 -2
  100. data/lib/clacky/web/features/new-session/view.js +49 -12
  101. data/lib/clacky/web/features/workspace/view.js +712 -77
  102. data/lib/clacky/web/i18n.js +80 -10
  103. data/lib/clacky/web/index.html +38 -15
  104. data/lib/clacky/web/sessions.js +538 -138
  105. data/lib/clacky/web/settings.js +32 -15
  106. data/lib/clacky/web/skills.js +11 -17
  107. data/lib/clacky/web/ws-dispatcher.js +22 -8
  108. data/lib/clacky.rb +4 -1
  109. metadata +15 -2
  110. data/lib/clacky/tools/request_user_feedback.rb +0 -84
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '038138bc72f3f740ed3ccee6da3f6464f80fd0cf1d0466788879a4472ac127b1'
4
- data.tar.gz: dfb75e0b2e832e923c34a1f5e61b4a005bd29435f3724763128b0fadc0b6ec1b
3
+ metadata.gz: 858730306a6e6647d79a5ebb35160c52fba7ed26c62ec97043373e7f052332e2
4
+ data.tar.gz: b412bc405daae81f1b6d71dd4dd2dffc197a00ac97540ecfd4916f7daf58843d
5
5
  SHA512:
6
- metadata.gz: c72531566a2b744a279e54907f750ff67e518963c9d42216dde3cb05048e4c86688dbb2ebfe908b31b7f7df8d2332df0ab0b11e0977ab5c486ca5c83b565eafc
7
- data.tar.gz: 584975067a26ebfc123e9f9a7f168ef45aab93f783e91da576510f416554c0ba4598e88064db7f71c0f6a30dfda949a4f36cc35d69aa35973489be800422a6b2
6
+ metadata.gz: 685d67556e68efd81a8bc446edd473a57939f0404b754e4432c7d29e073d2c47adc0c939fc5da3ea32144f3c6026aa17d73ff4766420399515089a3a8a4bc98d
7
+ data.tar.gz: dc85168d4b103185d20087635cfc5972c4e3e23b8dee95cb2ba28017d276dda425d8d92d23159dad463bd6cdcc29b8e02404d50dd4a79e5e6345193e8b4167c8
data/CHANGELOG.md CHANGED
@@ -4,6 +4,66 @@ 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.12] - 2026-08-27
8
+
9
+ ### Added
10
+ - Add VS Code-style file tree with inline file viewer
11
+ - Add quick-access sidebar for directory picker with WSL support
12
+ - Add Web UI @mention references for files, directories and sessions
13
+ - Add Gemini 3.7 Flash model with STT/video support; switch OCR default to DeepSeek vision
14
+ - Add timestamps to assistant messages
15
+ - Merge platform usage API data with local billing records
16
+ - Refresh git panel on session completion
17
+ - Allow disabling local extensions
18
+ - Expose task mappings in core events (#496 - @YeEmrick)
19
+
20
+ ### Improved
21
+ - Move file-trash sweep off the session save path and speed up session creation
22
+ - Make recharge link a visible button on model cards
23
+
24
+ ### Fixed
25
+ - Fix ask_user free-text input: render without options, guard IME composition, route Enter, render as text in IM channels, keep questions concise (#497 - @afumu)
26
+ - Fix advisor recommendations: send structured options, keep card state per session, dismiss button no longer permanently suppresses
27
+ - Fix home favorite pointing at the Windows profile under WSL
28
+ - Fix session list pagination and pin truncation
29
+ - Preserve task ids in compressed history (#500 - @YeEmrick)
30
+ - Clear stale media model override when reverting to auto
31
+ - Normalize gemini-3.7-flash name for pricing
32
+ - Fix sidebar scrollbar visibility and session list styling
33
+ - Fix messages loading created_at
34
+ - Render parallel fan-out subagent phases as flat, isolated cards
35
+ - Preserve subagent transcripts when an interrupted fan-out is resumed
36
+ - Fix spinner in subagent raise process
37
+ - Truncate messages carrying the system_injected flag
38
+ - Stop reporting awaiting_feedback for question-mark endings
39
+ - Detach channel_ui and keep channel_info unique on /bind and /unbind
40
+
41
+ ## [1.5.11] - 2026-08-23
42
+
43
+ ### Added
44
+ - Creator Studio: separate Marketplace and Brand sections for published extensions
45
+ - Advisor extension for next-job suggestions
46
+ - ask_user tool with multi-question support (replaces feedback_user)
47
+ - Sidebar states for awaiting-feedback and just-finished sessions
48
+ - Auto-purge file-trash entries older than 8 days
49
+ - DeepSeek V4 flash vision exp model preset
50
+ - IM channel toggle to mute tool-call narration and previews
51
+
52
+ ### Improved
53
+ - Unified internal thread management with graceful shutdown
54
+
55
+ ### Fixed
56
+ - Skill decrypt retry now picks up a renewed license key
57
+ - DeepSeek weekends billed entirely at off-peak rates
58
+ - Skill and extension zip downloads fall back to a secondary CDN host
59
+ - Session status dot aligned with name and channel badge
60
+ - Project session tag top margin
61
+ - Unified modal close button and ext-studio modal semantics
62
+ - Unified checkbox accent and distinct verified badge
63
+ - Edit-model modal overflow on small screens
64
+ - Subagent UI fixes
65
+ - Star History charts migrated to star-history.dera.page (#491 - @CrustyMozarella)
66
+
7
67
  ## [1.5.10] - 2026-08-19
8
68
 
9
69
  ### Added
data/README.md CHANGED
@@ -209,11 +209,11 @@ $ openclacky
209
209
 
210
210
  ## Star History
211
211
 
212
- <a href="https://www.star-history.com/?repos=clacky-ai%2Fopenclacky&type=date&legend=top-left">
212
+ <a href="https://star-history.dera.page/#clacky-ai/openclacky&type=date&legend=top-left">
213
213
  <picture>
214
- <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=clacky-ai/openclacky&type=date&theme=dark&legend=top-left" />
215
- <source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=clacky-ai/openclacky&type=date&legend=top-left" />
216
- <img alt="Star History Chart" src="https://api.star-history.com/chart?repos=clacky-ai/openclacky&type=date&legend=top-left" />
214
+ <source media="(prefers-color-scheme: dark)" srcset="https://star-history.dera.page/svg?repos=clacky-ai/openclacky&type=date&theme=dark&legend=top-left" />
215
+ <source media="(prefers-color-scheme: light)" srcset="https://star-history.dera.page/svg?repos=clacky-ai/openclacky&type=date&legend=top-left" />
216
+ <img alt="Star History Chart" src="https://star-history.dera.page/svg?repos=clacky-ai/openclacky&type=date&legend=top-left" />
217
217
  </picture>
218
218
  </a>
219
219
 
data/README_CN.md CHANGED
@@ -204,11 +204,11 @@ $ openclacky
204
204
 
205
205
  ## Star 历史
206
206
 
207
- <a href="https://www.star-history.com/?repos=clacky-ai%2Fopenclacky&type=date&legend=top-left">
207
+ <a href="https://star-history.dera.page/#clacky-ai/openclacky&type=date&legend=top-left">
208
208
  <picture>
209
- <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=clacky-ai/openclacky&type=date&theme=dark&legend=top-left" />
210
- <source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=clacky-ai/openclacky&type=date&legend=top-left" />
211
- <img alt="Star History Chart" src="https://api.star-history.com/chart?repos=clacky-ai/openclacky&type=date&legend=top-left" />
209
+ <source media="(prefers-color-scheme: dark)" srcset="https://star-history.dera.page/svg?repos=clacky-ai/openclacky&type=date&theme=dark&legend=top-left" />
210
+ <source media="(prefers-color-scheme: light)" srcset="https://star-history.dera.page/svg?repos=clacky-ai/openclacky&type=date&legend=top-left" />
211
+ <img alt="Star History Chart" src="https://star-history.dera.page/svg?repos=clacky-ai/openclacky&type=date&legend=top-left" />
212
212
  </picture>
213
213
  </a>
214
214
 
data/README_JA.md CHANGED
@@ -209,11 +209,11 @@ $ openclacky
209
209
 
210
210
  ## Star History
211
211
 
212
- <a href="https://www.star-history.com/?repos=clacky-ai%2Fopenclacky&type=date&legend=top-left">
212
+ <a href="https://star-history.dera.page/#clacky-ai/openclacky&type=date&legend=top-left">
213
213
  <picture>
214
- <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=clacky-ai/openclacky&type=date&theme=dark&legend=top-left" />
215
- <source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=clacky-ai/openclacky&type=date&legend=top-left" />
216
- <img alt="Star History Chart" src="https://api.star-history.com/chart?repos=clacky-ai/openclacky&type=date&legend=top-left" />
214
+ <source media="(prefers-color-scheme: dark)" srcset="https://star-history.dera.page/svg?repos=clacky-ai/openclacky&type=date&theme=dark&legend=top-left" />
215
+ <source media="(prefers-color-scheme: light)" srcset="https://star-history.dera.page/svg?repos=clacky-ai/openclacky&type=date&legend=top-left" />
216
+ <img alt="Star History Chart" src="https://star-history.dera.page/svg?repos=clacky-ai/openclacky&type=date&legend=top-left" />
217
217
  </picture>
218
218
  </a>
219
219
 
@@ -136,11 +136,13 @@ module Clacky
136
136
  # This ensures all API calls are recorded for statistics purposes.
137
137
  billing_model = model || current_model
138
138
  effective_cost = cost || 0.0 # Use 0 if pricing is unknown
139
+ provider = @config.provider_id_for(@config.current_model)
139
140
 
140
141
  record = Billing::BillingRecord.new(
141
142
  session_id: @session_id,
142
143
  timestamp: Time.now,
143
144
  model: billing_model,
145
+ provider: provider,
144
146
  prompt_tokens: usage[:prompt_tokens] || 0,
145
147
  completion_tokens: usage[:completion_tokens] || 0,
146
148
  cache_read_tokens: usage[:cache_read_input_tokens] || 0,
@@ -95,6 +95,7 @@ module Clacky
95
95
 
96
96
  begin
97
97
  begin
98
+ Clacky::Shutdown.checkpoint!
98
99
  # Use active_messages (Time Machine) when undone, otherwise send full history.
99
100
  # to_api strips internal fields and handles orphaned tool_calls.
100
101
  messages_to_send = if respond_to?(:active_messages)
@@ -167,6 +168,7 @@ module Clacky
167
168
  end
168
169
 
169
170
  rescue Faraday::TimeoutError => e
171
+ Clacky::Shutdown.checkpoint!
170
172
  # Faraday::TimeoutError on our non-streaming POST almost always means
171
173
  # the *response* took longer than the 300s read-timeout to come back —
172
174
  # i.e. the model is trying to produce a huge output in one shot
@@ -198,7 +200,7 @@ module Clacky
198
200
  metadata: { attempt: retries, total: max_retries }
199
201
  )
200
202
  retrying_progress_opened = true
201
- sleep retry_delay
203
+ Clacky::Shutdown.sleep(retry_delay)
202
204
  retry
203
205
  else
204
206
  # All retries exhausted for a timeout. Try the secondary gateway URL if available.
@@ -219,6 +221,7 @@ module Clacky
219
221
  end
220
222
 
221
223
  rescue Faraday::ConnectionFailed, Faraday::SSLError, Errno::ECONNREFUSED, Errno::ETIMEDOUT, Errno::EPIPE => e
224
+ Clacky::Shutdown.checkpoint!
222
225
  retries += 1
223
226
 
224
227
  # Errno::EPIPE means the underlying TCP socket is dead (server closed the
@@ -245,7 +248,7 @@ module Clacky
245
248
  metadata: { attempt: retries, total: max_retries }
246
249
  )
247
250
  retrying_progress_opened = true
248
- sleep retry_delay
251
+ Clacky::Shutdown.sleep(retry_delay)
249
252
  retry
250
253
  else
251
254
  # All retries on the primary endpoint exhausted for a network-level
@@ -269,6 +272,7 @@ module Clacky
269
272
  end
270
273
 
271
274
  rescue RetryableError => e
275
+ Clacky::Shutdown.checkpoint!
272
276
  retries += 1
273
277
 
274
278
  # Probing failure: primary still down — renew cooling-off and retry with fallback.
@@ -298,7 +302,7 @@ module Clacky
298
302
  metadata: { attempt: retries, total: current_max }
299
303
  )
300
304
  retrying_progress_opened = true
301
- sleep retry_delay
305
+ Clacky::Shutdown.sleep(retry_delay)
302
306
  retry
303
307
  else
304
308
  # All retries on the current endpoint exhausted.
@@ -624,7 +624,7 @@ module Clacky
624
624
 
625
625
  case role
626
626
  when "user"
627
- lines << "## User"
627
+ lines << (msg[:task_id] ? "## User [Task #{msg[:task_id]}]" : "## User")
628
628
  lines << ""
629
629
  lines << format_message_content(content)
630
630
  lines << ""
@@ -391,7 +391,11 @@ module Clacky
391
391
  preview_path: f[:preview_path] || f["preview_path"] }
392
392
  }
393
393
  all_files = image_files + disk_files
394
- ui.show_user_message(raw_text, created_at: msg[:created_at], files: all_files,
394
+ references = Array(msg[:display_references])
395
+ ref_options = references.empty? ? {} : { references: references }
396
+ task_options = msg[:task_id] ? { task_id: msg[:task_id] } : {}
397
+ ui.show_user_message(raw_text, **task_options, created_at: msg[:created_at], files: all_files,
398
+ **ref_options,
395
399
  editable: round[:editable] != false,
396
400
  skill_command: msg[:skill_command],
397
401
  skill_command_display: msg[:skill_command_display])
@@ -490,30 +494,34 @@ module Clacky
490
494
  current_role = nil
491
495
  current_lines = []
492
496
  current_nested_chunk = nil # chunk reference from a Compressed Summary heading
497
+ current_task_id = nil
493
498
 
494
499
  raw.each_line do |line|
495
500
  stripped = line.chomp
496
501
  if (m = stripped.match(/\A## Assistant \[Compressed Summary — original conversation at: (.+)\]/))
497
502
  # Nested chunk reference — record it, treat as assistant section
498
- sections << { role: current_role, lines: current_lines.dup, nested_chunk: current_nested_chunk } if current_role
503
+ sections << { role: current_role, lines: current_lines.dup, nested_chunk: current_nested_chunk, task_id: current_task_id } if current_role
499
504
  current_role = "assistant"
500
505
  current_lines = []
501
506
  current_nested_chunk = File.join(chunk_dir, m[1])
502
- elsif stripped.match?(/\A## (User|Assistant)/)
503
- sections << { role: current_role, lines: current_lines.dup, nested_chunk: current_nested_chunk } if current_role
504
- current_role = stripped.match(/\A## (User|Assistant)/)[1].downcase
507
+ current_task_id = nil
508
+ elsif (m = stripped.match(/\A## (User|Assistant)(?: \[Task ([1-9]\d*)\])?\z/))
509
+ sections << { role: current_role, lines: current_lines.dup, nested_chunk: current_nested_chunk, task_id: current_task_id } if current_role
510
+ current_role = m[1].downcase
505
511
  current_lines = []
506
512
  current_nested_chunk = nil
513
+ current_task_id = m[2]&.to_i
507
514
  elsif stripped.match?(/\A### Tool Result:/)
508
- sections << { role: current_role, lines: current_lines.dup, nested_chunk: current_nested_chunk } if current_role
515
+ sections << { role: current_role, lines: current_lines.dup, nested_chunk: current_nested_chunk, task_id: current_task_id } if current_role
509
516
  current_role = "tool"
510
517
  current_lines = []
511
518
  current_nested_chunk = nil
519
+ current_task_id = nil
512
520
  else
513
521
  current_lines << line
514
522
  end
515
523
  end
516
- sections << { role: current_role, lines: current_lines.dup, nested_chunk: current_nested_chunk } if current_role
524
+ sections << { role: current_role, lines: current_lines.dup, nested_chunk: current_nested_chunk, task_id: current_task_id } if current_role
517
525
 
518
526
  # Remove front-matter / header noise sections (nil role or non-user/assistant/tool)
519
527
  sections.select! { |s| %w[user assistant tool].include?(s[:role]) }
@@ -544,14 +552,16 @@ module Clacky
544
552
  round_index += 1
545
553
  # Synthetic timestamp: spread rounds backwards from archived_at
546
554
  synthetic_ts = base_time - (sections.size - round_index) * 1.0
555
+ user_msg = {
556
+ role: "user",
557
+ content: text,
558
+ created_at: synthetic_ts,
559
+ ext_events: sec_ext_events,
560
+ _from_chunk: true
561
+ }
562
+ user_msg[:task_id] = sec[:task_id] if sec[:task_id]
547
563
  current_round = {
548
- user_msg: {
549
- role: "user",
550
- content: text,
551
- created_at: synthetic_ts,
552
- ext_events: sec_ext_events,
553
- _from_chunk: true
554
- },
564
+ user_msg: user_msg,
555
565
  events: [],
556
566
  # editable: false — this message was archived into a chunk MD and no
557
567
  # longer exists in the active in-memory @history, so it cannot be
@@ -733,7 +743,7 @@ module Clacky
733
743
  else
734
744
  raw_text
735
745
  end
736
- ui.show_assistant_message(text, files: [])
746
+ ui.show_assistant_message(text, files: [], created_at: msg[:created_at])
737
747
  end
738
748
 
739
749
  # Tool calls embedded in assistant message
@@ -742,16 +752,17 @@ module Clacky
742
752
  args_raw = tc[:arguments] || tc.dig(:function, :arguments) || {}
743
753
  args = args_raw.is_a?(String) ? (JSON.parse(args_raw) rescue args_raw) : args_raw
744
754
 
745
- # Special handling: request_user_feedback question is shown as an
746
- # assistant message (matching real-time behavior), not as a tool call.
747
- # Reconstruct the full formatted message including options (mirrors RequestUserFeedback#execute).
748
- if name == "request_user_feedback"
749
- question = args.is_a?(Hash) ? (args[:question] || args["question"]).to_s : ""
750
- context = args.is_a?(Hash) ? (args[:context] || args["context"]).to_s : ""
751
- options = args.is_a?(Hash) ? (args[:options] || args["options"]) : nil
752
- options = Array(options) if options && !options.is_a?(Array)
755
+ # Special handling: the ask_user question is shown as an assistant
756
+ # message (matching real-time behavior), not as a tool call.
757
+ if Clacky::Tools::AskUser.feedback_tool?(name)
758
+ questions = Clacky::Tools::AskUser.normalize_questions(args)
759
+ context = Clacky::Tools::AskUser.fetch_key(args, :context).to_s
760
+ first = questions.first
753
761
 
754
- ui.show_feedback_request(question, context, options || []) unless question.empty?
762
+ unless first.nil?
763
+ ui.show_feedback_request(first[:question], context, first[:options],
764
+ questions: questions)
765
+ end
755
766
  else
756
767
  ui.show_tool_call(name, args)
757
768
  end
@@ -760,6 +771,11 @@ module Clacky
760
771
  # Emit token usage stored on this message (for history replay display)
761
772
  ui.show_token_usage(msg[:token_usage]) if msg[:token_usage]
762
773
 
774
+ # Interrupted fan-out anchors its captured subagent trails onto the
775
+ # last message, which is usually this assistant turn (its tool result
776
+ # never got written). Replay them here so they survive a reload.
777
+ replay_subagent_transcript(msg, ui)
778
+
763
779
  when "user"
764
780
  # Anthropic-format tool results (role: user, content: array of tool_result blocks)
765
781
  if msg[:content].is_a?(Array)
@@ -796,19 +812,32 @@ module Clacky
796
812
  end
797
813
  end
798
814
 
799
- # Replay a subagent transcript stored on a tool result message. Emits a
815
+ # Replay subagent transcripts stored on a tool result message. Emits a
800
816
  # bracketed sequence of UI events the frontend can render as a collapsible
801
817
  # sub-process block: subagent_start → (assistant_message / tool_call /
802
- # tool_result)* → subagent_end. No-op when the message carries no transcript.
818
+ # tool_result)* → subagent_end. A fan-out batch renders one block per job,
819
+ # in the caller's original job order.
820
+ #
821
+ # Sessions saved before fan-out support stored a bare Hash here.
803
822
  def replay_subagent_transcript(msg, ui)
804
- transcript = msg[:subagent_transcript]
805
- return unless transcript.is_a?(Hash)
823
+ stored = msg[:subagent_transcript]
824
+ Array(stored.is_a?(Hash) ? [stored] : stored).each do |transcript|
825
+ next unless transcript.is_a?(Hash)
806
826
 
827
+ replay_one_subagent_transcript(transcript, ui)
828
+ end
829
+ end
830
+
831
+ def replay_one_subagent_transcript(transcript, ui)
807
832
  events = transcript[:events] || transcript["events"] || []
808
833
  return if events.empty?
809
834
 
835
+ skill = transcript[:skill] || transcript["skill"]
836
+ supports_phase = ui.respond_to?(:phase_start) && ui.respond_to?(:phase_end)
837
+ pid = ui.phase_start(kind: "fanout_subagent", label: skill) if supports_phase
838
+
810
839
  ui.show_subagent_start(
811
- skill: transcript[:skill] || transcript["skill"],
840
+ skill: skill,
812
841
  iterations: transcript[:iterations] || transcript["iterations"],
813
842
  cost_usd: transcript[:cost_usd] || transcript["cost_usd"]
814
843
  )
@@ -821,7 +850,7 @@ module Clacky
821
850
  case role
822
851
  when "assistant"
823
852
  text = extract_text_from_content(content).to_s.strip
824
- ui.show_assistant_message(text, files: []) unless text.empty?
853
+ ui.show_assistant_message(text, files: [], created_at: ev[:created_at] || ev["created_at"]) unless text.empty?
825
854
  Array(tool_calls).each do |tc|
826
855
  name = tc[:name] || tc["name"] || ""
827
856
  args_raw = tc[:arguments] || tc["arguments"] || {}
@@ -834,6 +863,7 @@ module Clacky
834
863
  end
835
864
 
836
865
  ui.show_subagent_end
866
+ ui.phase_end(pid) if supports_phase && pid
837
867
  end
838
868
 
839
869
  # Replace the system message in @messages with a freshly built system prompt.
@@ -651,8 +651,7 @@ module Clacky
651
651
  # observe() attaches each trail to its own tool result message.
652
652
  # Kept out of the LLM payload via INTERNAL_FIELDS.
653
653
  if tool_call_id
654
- @pending_subagent_transcripts[tool_call_id] =
655
- extract_subagent_transcript(subagent, skill.identifier)
654
+ record_subagent_transcript(tool_call_id, subagent, skill.identifier)
656
655
  end
657
656
 
658
657
  # Mutate the subagent_instructions message in-place to become the result summary
@@ -589,6 +589,7 @@ module Clacky
589
589
 
590
590
  tasks << {
591
591
  task_id: task_id,
592
+ parent_task_id: @task_parents[task_id].to_i,
592
593
  summary: summary,
593
594
  started_at: meta[:started_at],
594
595
  ended_at: meta[:ended_at],
@@ -13,7 +13,7 @@ module Clacky
13
13
  case @config.permission_mode
14
14
  when :auto_approve, :confirm_all
15
15
  # Both modes auto-execute all file/shell tools without confirmation.
16
- # The difference is only in request_user_feedback handling:
16
+ # The difference is only in ask_user handling:
17
17
  # auto_approve → no human present, inject auto_reply
18
18
  # confirm_all → human present, truly wait for user input
19
19
  true
@@ -55,10 +55,12 @@ module Clacky
55
55
  # todo_manager aliases
56
56
  "todo" => "todo_manager",
57
57
  "task_manager" => "todo_manager",
58
- # request_user_feedback aliases
59
- "ask_user" => "request_user_feedback",
60
- "user_feedback" => "request_user_feedback",
61
- "ask" => "request_user_feedback",
58
+ # ask_user aliases
59
+ "request_user_feedback" => "ask_user",
60
+ "user_feedback" => "ask_user",
61
+ "ask" => "ask_user",
62
+ "ask_question" => "ask_user",
63
+ "clarify" => "ask_user",
62
64
  # trash_manager aliases
63
65
  "trash" => "trash_manager",
64
66
  "delete" => "trash_manager",