openclacky 1.5.11 → 1.5.13
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 +82 -0
- data/lib/clacky/access_key.rb +59 -0
- data/lib/clacky/agent/cost_tracker.rb +2 -0
- data/lib/clacky/agent/message_compressor_helper.rb +53 -2
- data/lib/clacky/agent/session_serializer.rb +72 -19
- data/lib/clacky/agent/time_machine.rb +10 -1
- data/lib/clacky/agent.rb +107 -26
- data/lib/clacky/agent_config.rb +40 -0
- data/lib/clacky/agent_profile.rb +5 -5
- data/lib/clacky/billing/billing_record.rb +3 -0
- data/lib/clacky/billing/billing_store.rb +31 -5
- data/lib/clacky/billing/platform_billing.rb +231 -0
- data/lib/clacky/brand_config.rb +4 -3
- data/lib/clacky/cancel_flag.rb +23 -0
- data/lib/clacky/cli.rb +4 -2
- data/lib/clacky/client.rb +107 -0
- data/lib/clacky/default_extensions/advisor/advisors/general.md +23 -17
- data/lib/clacky/default_extensions/advisor/hooks/advisor.rb +104 -5
- data/lib/clacky/default_extensions/advisor/panels/advisor/view.js +92 -75
- data/lib/clacky/default_extensions/ext-studio/api/handler.rb +31 -0
- data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +74 -24
- data/lib/clacky/default_extensions/ext-studio/skills/ext-develop/SKILL.md +16 -37
- data/lib/clacky/default_extensions/git/ext.yml +1 -1
- data/lib/clacky/default_extensions/git/panels/git/view.js +483 -67
- data/lib/clacky/default_extensions/meeting/ext.yml +2 -1
- data/lib/clacky/default_extensions/time_machine/ext.yml +1 -1
- data/lib/clacky/default_extensions/time_machine/panels/time_machine/view.js +186 -208
- data/lib/clacky/extension/packager.rb +4 -2
- data/lib/clacky/extension/scaffold/templates/full/panels/dashboard/view.js.erb +2 -3
- data/lib/clacky/extension/verifier.rb +14 -1
- data/lib/clacky/fanout.rb +49 -2
- data/lib/clacky/json_ui_controller.rb +3 -2
- data/lib/clacky/message_format/open_ai.rb +5 -1
- data/lib/clacky/message_format/open_ai_responses.rb +409 -0
- data/lib/clacky/message_history.rb +43 -1
- data/lib/clacky/openai_responses_stream_aggregator.rb +294 -0
- data/lib/clacky/plain_ui_controller.rb +15 -5
- data/lib/clacky/providers.rb +24 -7
- data/lib/clacky/rich_ui/rich_ui_controller.rb +2 -2
- data/lib/clacky/server/channel/channel_manager.rb +93 -18
- data/lib/clacky/server/channel/channel_ui_controller.rb +16 -3
- data/lib/clacky/server/dir_picker.rb +154 -0
- data/lib/clacky/server/git_panel.rb +61 -9
- data/lib/clacky/server/http_server.rb +366 -42
- data/lib/clacky/server/web_ui_controller.rb +7 -5
- data/lib/clacky/session_manager.rb +74 -17
- data/lib/clacky/tools/ask_user.rb +3 -2
- data/lib/clacky/tools/terminal.rb +30 -3
- data/lib/clacky/ui2/markdown_renderer.rb +15 -12
- data/lib/clacky/ui2/strings_cjk_patch.rb +132 -0
- data/lib/clacky/ui2/themes/hacker_theme.rb +1 -1
- data/lib/clacky/ui2/themes/minimal_theme.rb +1 -1
- data/lib/clacky/ui2/ui_controller.rb +2 -2
- data/lib/clacky/ui_interface.rb +2 -2
- data/lib/clacky/utils/model_pricing.rb +92 -4
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +915 -142
- data/lib/clacky/web/components/code-editor.js +33 -1
- data/lib/clacky/web/components/composer.js +315 -0
- data/lib/clacky/web/components/custom-select.js +136 -0
- data/lib/clacky/web/components/mentions.js +655 -0
- data/lib/clacky/web/components/model-picker.js +527 -0
- data/lib/clacky/web/components/onboard.js +29 -55
- data/lib/clacky/web/core/aside.js +3 -3
- data/lib/clacky/web/core/ext.js +38 -0
- data/lib/clacky/web/features/billing/store.js +2 -6
- data/lib/clacky/web/features/billing/view.js +32 -16
- data/lib/clacky/web/features/extensions/view.js +22 -10
- data/lib/clacky/web/features/new-session/store.js +15 -1
- data/lib/clacky/web/features/new-session/view.js +80 -34
- data/lib/clacky/web/features/trash/view.js +31 -19
- data/lib/clacky/web/features/workspace/store.js +1 -0
- data/lib/clacky/web/features/workspace/view.js +766 -80
- data/lib/clacky/web/i18n.js +107 -17
- data/lib/clacky/web/index.html +125 -45
- data/lib/clacky/web/sessions.js +704 -635
- data/lib/clacky/web/settings.js +164 -190
- data/lib/clacky/web/skills.js +11 -17
- data/lib/clacky/web/ws-dispatcher.js +11 -33
- data/lib/clacky.rb +4 -0
- 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: ca259b2f17df9695e21c4d4c4f18ee83affec0fc41d67db829d903e1c3bb008d
|
|
4
|
+
data.tar.gz: b0bfd31378c6b452a7aef246d3d316242a91fa6366f543c45edcff8c3f44ac6c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cfcb5b28264aeb2acb6b121b89c395e4e9241b3a3135c2e9fb7d565365ed263c701f939448a2c01cb229359cca11402b36b081d4edff6231ce8e46e72d253498
|
|
7
|
+
data.tar.gz: 95cca310ebd8e0a23b1f1e5aed241b6e5fbc19aa206fdb16301dfd3b90f07e33424a44686c10b2d6cf4d3344c75eab2c033d7b3e4e7ef6c9a40b7879dd07a88b
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,88 @@ 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.13] - 2026-09-03
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- Add Gemini 3.8 Flash model and set it as the trial default
|
|
11
|
+
- Add or-stt-gemini-3-8-flash speech-to-text model; default STT and video understanding to 3.8
|
|
12
|
+
- Add OpenAI Responses API as a fourth API format (#490 - @kylezhang)
|
|
13
|
+
- Add access-key API and config-reload API
|
|
14
|
+
- Add Claude Fable 5.1, GLM-5.3-Flash and qwen3.8-max models with pricing
|
|
15
|
+
- Default the Web UI to the OS/browser language on first visit
|
|
16
|
+
- Make the workspace file tree resizable
|
|
17
|
+
- Expose mounted drives in the directory picker
|
|
18
|
+
- Personalize the new-session page title and empty-chat greeting with the AI's name
|
|
19
|
+
- Localise built-in setup session titles
|
|
20
|
+
|
|
21
|
+
### Improved
|
|
22
|
+
- Replace native selects with shared CustomSelect dropdowns across settings
|
|
23
|
+
- Overhaul the git & time-machine panel
|
|
24
|
+
- Improve the directory picker (#521)
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
- Merge OpenClacky billing across all configured API keys
|
|
28
|
+
- Preserve custom provider and API format when editing a model
|
|
29
|
+
- Reclaim PTY file descriptors when a session shell dies
|
|
30
|
+
- Wrap ANSI-coloured CJK text without IndexError
|
|
31
|
+
- Prevent history edits while a session is running (#520)
|
|
32
|
+
- Preserve attachment badges after compression (#516)
|
|
33
|
+
- Flag hidden agents instead of dropping them
|
|
34
|
+
- Install upgrades next to the running gem's base directory
|
|
35
|
+
- Skip extension auto-sync for administrators (#515)
|
|
36
|
+
- Support the mention menu with Windows IME (#513)
|
|
37
|
+
- Validate extension version input (#514)
|
|
38
|
+
- Keep mention chips anchored and on one line while typing
|
|
39
|
+
- Fall back to uncolored markdown for CJK tables
|
|
40
|
+
- Stabilise billing toolbar layout (#517)
|
|
41
|
+
- Light the group unread dot for backend-created sessions (#518)
|
|
42
|
+
- Translate maximum reasoning effort (#522)
|
|
43
|
+
- Harden WSL Windows profile detection in the directory picker
|
|
44
|
+
- Wrap download errors so the CDN fallback fires
|
|
45
|
+
- Surface the git tab badge eagerly
|
|
46
|
+
- Show the meeting panel only in general sessions
|
|
47
|
+
- Whitelist the config top-level key in the extension verifier
|
|
48
|
+
|
|
49
|
+
### More
|
|
50
|
+
- Extract shared ModelPicker model dropdown component
|
|
51
|
+
- Refine dropdown chevrons and submenu selection
|
|
52
|
+
- Adjust sidebar panel order and labels
|
|
53
|
+
- Update trial model copy to Gemini 3.7 Flash
|
|
54
|
+
|
|
55
|
+
## [1.5.12] - 2026-08-27
|
|
56
|
+
|
|
57
|
+
### Added
|
|
58
|
+
- Add VS Code-style file tree with inline file viewer
|
|
59
|
+
- Add quick-access sidebar for directory picker with WSL support
|
|
60
|
+
- Add Web UI @mention references for files, directories and sessions
|
|
61
|
+
- Add Gemini 3.7 Flash model with STT/video support; switch OCR default to DeepSeek vision
|
|
62
|
+
- Add timestamps to assistant messages
|
|
63
|
+
- Merge platform usage API data with local billing records
|
|
64
|
+
- Refresh git panel on session completion
|
|
65
|
+
- Allow disabling local extensions
|
|
66
|
+
- Expose task mappings in core events (#496 - @YeEmrick)
|
|
67
|
+
|
|
68
|
+
### Improved
|
|
69
|
+
- Move file-trash sweep off the session save path and speed up session creation
|
|
70
|
+
- Make recharge link a visible button on model cards
|
|
71
|
+
|
|
72
|
+
### Fixed
|
|
73
|
+
- 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)
|
|
74
|
+
- Fix advisor recommendations: send structured options, keep card state per session, dismiss button no longer permanently suppresses
|
|
75
|
+
- Fix home favorite pointing at the Windows profile under WSL
|
|
76
|
+
- Fix session list pagination and pin truncation
|
|
77
|
+
- Preserve task ids in compressed history (#500 - @YeEmrick)
|
|
78
|
+
- Clear stale media model override when reverting to auto
|
|
79
|
+
- Normalize gemini-3.7-flash name for pricing
|
|
80
|
+
- Fix sidebar scrollbar visibility and session list styling
|
|
81
|
+
- Fix messages loading created_at
|
|
82
|
+
- Render parallel fan-out subagent phases as flat, isolated cards
|
|
83
|
+
- Preserve subagent transcripts when an interrupted fan-out is resumed
|
|
84
|
+
- Fix spinner in subagent raise process
|
|
85
|
+
- Truncate messages carrying the system_injected flag
|
|
86
|
+
- Stop reporting awaiting_feedback for question-mark endings
|
|
87
|
+
- Detach channel_ui and keep channel_info unique on /bind and /unbind
|
|
88
|
+
|
|
7
89
|
## [1.5.11] - 2026-08-23
|
|
8
90
|
|
|
9
91
|
### Added
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "securerandom"
|
|
5
|
+
|
|
6
|
+
module Clacky
|
|
7
|
+
# Access key for server-mode authentication.
|
|
8
|
+
#
|
|
9
|
+
# Two sources, in priority order:
|
|
10
|
+
# 1. ~/.clacky/access_key — a file holding the raw key, writable from
|
|
11
|
+
# outside the process (e.g. a sandbox host pushing a key into a
|
|
12
|
+
# pre-started container). This is what makes rotation possible:
|
|
13
|
+
# ENV is a boot-time snapshot the running process can never change.
|
|
14
|
+
# 2. CLACKY_ACCESS_KEY env var — the original bootstrap mechanism.
|
|
15
|
+
module AccessKey
|
|
16
|
+
ENV_VAR = "CLACKY_ACCESS_KEY"
|
|
17
|
+
|
|
18
|
+
def self.resolve
|
|
19
|
+
from_file || from_env
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.from_file
|
|
23
|
+
return nil unless File.exist?(key_file)
|
|
24
|
+
|
|
25
|
+
key = File.read(key_file).strip
|
|
26
|
+
key.empty? ? nil : key
|
|
27
|
+
rescue StandardError
|
|
28
|
+
nil
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.from_env
|
|
32
|
+
key = ENV.fetch(ENV_VAR, "").strip
|
|
33
|
+
key.empty? ? nil : key
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.write(key)
|
|
37
|
+
key = key.to_s.strip
|
|
38
|
+
raise ArgumentError, "access key must not be blank" if key.empty?
|
|
39
|
+
|
|
40
|
+
FileUtils.mkdir_p(File.dirname(key_file))
|
|
41
|
+
File.write(key_file, "#{key}\n")
|
|
42
|
+
FileUtils.chmod(0o600, key_file)
|
|
43
|
+
key
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def self.generate
|
|
47
|
+
SecureRandom.hex(32)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.configured?
|
|
51
|
+
!resolve.nil?
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Resolved lazily so specs that stub Dir.home in a before-hook still work.
|
|
55
|
+
def self.key_file
|
|
56
|
+
File.join(Dir.home, ".clacky", "access_key")
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -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,
|
|
@@ -624,10 +624,12 @@ module Clacky
|
|
|
624
624
|
|
|
625
625
|
case role
|
|
626
626
|
when "user"
|
|
627
|
-
|
|
627
|
+
display_files = display_files_for_archive(msg)
|
|
628
|
+
lines << (msg[:task_id] ? "## User [Task #{msg[:task_id]}]" : "## User")
|
|
628
629
|
lines << ""
|
|
629
|
-
lines <<
|
|
630
|
+
lines << format_archived_user_content(content, display_files)
|
|
630
631
|
lines << ""
|
|
632
|
+
append_display_files_line(lines, display_files)
|
|
631
633
|
append_ext_events_line(lines, msg)
|
|
632
634
|
when "assistant"
|
|
633
635
|
# If this message is itself a compressed summary, annotate the header
|
|
@@ -676,6 +678,55 @@ module Clacky
|
|
|
676
678
|
lines
|
|
677
679
|
end
|
|
678
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.
|
|
684
|
+
def display_files_for_archive(msg)
|
|
685
|
+
files = Array(msg[:display_files]).dup
|
|
686
|
+
Array(msg[:content]).each do |block|
|
|
687
|
+
next unless block.is_a?(Hash)
|
|
688
|
+
next unless %w[image image_url].include?((block[:type] || block["type"]).to_s)
|
|
689
|
+
|
|
690
|
+
name = block[:image_name] || block["image_name"]
|
|
691
|
+
if name.nil? || name.to_s.strip.empty?
|
|
692
|
+
path = block[:image_path] || block["image_path"]
|
|
693
|
+
name = File.basename(path.to_s).sub(/\A[0-9a-f]{16}_/, "") unless path.to_s.empty?
|
|
694
|
+
end
|
|
695
|
+
name = "image" if name.nil? || name.to_s.strip.empty?
|
|
696
|
+
files << { name: name, type: "image" }
|
|
697
|
+
end
|
|
698
|
+
|
|
699
|
+
files.filter_map do |file|
|
|
700
|
+
next unless file.is_a?(Hash)
|
|
701
|
+
|
|
702
|
+
name = file[:name] || file["name"]
|
|
703
|
+
next if name.nil? || name.to_s.strip.empty?
|
|
704
|
+
|
|
705
|
+
type = file[:type] || file["type"] || "file"
|
|
706
|
+
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]] }
|
|
709
|
+
end
|
|
710
|
+
|
|
711
|
+
# Once an image has a badge in the archive, omit the generic [image_url]
|
|
712
|
+
# placeholder from the visible replay text. Text blocks remain unchanged.
|
|
713
|
+
def format_archived_user_content(content, display_files)
|
|
714
|
+
return format_message_content(content) unless content.is_a?(Array)
|
|
715
|
+
return format_message_content(content) unless display_files.any? { |file| file[:type] == "image" }
|
|
716
|
+
|
|
717
|
+
visible_blocks = content.reject do |block|
|
|
718
|
+
block.is_a?(Hash) && %w[image image_url].include?((block[:type] || block["type"]).to_s)
|
|
719
|
+
end
|
|
720
|
+
format_message_content(visible_blocks)
|
|
721
|
+
end
|
|
722
|
+
|
|
723
|
+
def append_display_files_line(lines, files)
|
|
724
|
+
return if files.empty?
|
|
725
|
+
|
|
726
|
+
lines << "_Display files: #{files.to_json}_"
|
|
727
|
+
lines << ""
|
|
728
|
+
end
|
|
729
|
+
|
|
679
730
|
# Serialize persisted extension events into the chunk MD so they survive
|
|
680
731
|
# compression and can be replayed in place. Mirrors the "_Tool calls:"
|
|
681
732
|
# convention; parse_chunk_md_to_rounds reads it back.
|
|
@@ -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
|
-
|
|
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
|
-
|
|
503
|
-
|
|
504
|
-
current_role
|
|
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]) }
|
|
@@ -526,6 +534,7 @@ module Clacky
|
|
|
526
534
|
sections.each do |sec|
|
|
527
535
|
text = sec[:lines].join.strip
|
|
528
536
|
text, sec_ext_events = extract_ext_events_from_text(text)
|
|
537
|
+
text, sec_display_files = extract_display_files_from_text(text)
|
|
529
538
|
|
|
530
539
|
# Nested chunk: expand it recursively, prepend before current rounds
|
|
531
540
|
if sec[:nested_chunk]
|
|
@@ -538,20 +547,23 @@ module Clacky
|
|
|
538
547
|
next
|
|
539
548
|
end
|
|
540
549
|
|
|
541
|
-
next if text.empty? && sec_ext_events.empty?
|
|
550
|
+
next if text.empty? && sec_ext_events.empty? && sec_display_files.empty?
|
|
542
551
|
|
|
543
552
|
if sec[:role] == "user"
|
|
544
553
|
round_index += 1
|
|
545
554
|
# Synthetic timestamp: spread rounds backwards from archived_at
|
|
546
555
|
synthetic_ts = base_time - (sections.size - round_index) * 1.0
|
|
556
|
+
user_msg = {
|
|
557
|
+
role: "user",
|
|
558
|
+
content: text,
|
|
559
|
+
created_at: synthetic_ts,
|
|
560
|
+
ext_events: sec_ext_events,
|
|
561
|
+
_from_chunk: true
|
|
562
|
+
}
|
|
563
|
+
user_msg[:task_id] = sec[:task_id] if sec[:task_id]
|
|
564
|
+
user_msg[:display_files] = sec_display_files unless sec_display_files.empty?
|
|
547
565
|
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
|
-
},
|
|
566
|
+
user_msg: user_msg,
|
|
555
567
|
events: [],
|
|
556
568
|
# editable: false — this message was archived into a chunk MD and no
|
|
557
569
|
# longer exists in the active in-memory @history, so it cannot be
|
|
@@ -608,6 +620,35 @@ module Clacky
|
|
|
608
620
|
end
|
|
609
621
|
|
|
610
622
|
|
|
623
|
+
# Pull lightweight attachment badge metadata out of a chunk section.
|
|
624
|
+
# The marker is internal archive data and must not appear in replayed text.
|
|
625
|
+
# Only name + type are accepted; paths and file contents are never restored.
|
|
626
|
+
def extract_display_files_from_text(text)
|
|
627
|
+
return [text, []] unless text.include?("_Display files:")
|
|
628
|
+
|
|
629
|
+
files = []
|
|
630
|
+
kept = text.each_line.reject do |line|
|
|
631
|
+
match = line.strip.match(/\A_Display files:\s*(\[.*\])_?\z/i)
|
|
632
|
+
next false unless match
|
|
633
|
+
|
|
634
|
+
parsed = JSON.parse(match[1]) rescue []
|
|
635
|
+
Array(parsed).each do |file|
|
|
636
|
+
next unless file.is_a?(Hash)
|
|
637
|
+
|
|
638
|
+
name = file["name"] || file[:name]
|
|
639
|
+
next if name.nil? || name.to_s.strip.empty?
|
|
640
|
+
|
|
641
|
+
type = file["type"] || file[:type] || "file"
|
|
642
|
+
type = "file" if type.to_s.strip.empty?
|
|
643
|
+
files << { name: name.to_s, type: type.to_s }
|
|
644
|
+
end
|
|
645
|
+
true
|
|
646
|
+
end
|
|
647
|
+
|
|
648
|
+
[kept.join.strip, files]
|
|
649
|
+
end
|
|
650
|
+
|
|
651
|
+
|
|
611
652
|
# Pull the "_Ext events: type | {json}; ..._" line out of a chunk section,
|
|
612
653
|
# returning the remaining text plus the parsed events. Counterpart to
|
|
613
654
|
# MessageCompressorHelper#append_ext_events_line.
|
|
@@ -733,7 +774,7 @@ module Clacky
|
|
|
733
774
|
else
|
|
734
775
|
raw_text
|
|
735
776
|
end
|
|
736
|
-
ui.show_assistant_message(text, files: [])
|
|
777
|
+
ui.show_assistant_message(text, files: [], created_at: msg[:created_at])
|
|
737
778
|
end
|
|
738
779
|
|
|
739
780
|
# Tool calls embedded in assistant message
|
|
@@ -761,6 +802,11 @@ module Clacky
|
|
|
761
802
|
# Emit token usage stored on this message (for history replay display)
|
|
762
803
|
ui.show_token_usage(msg[:token_usage]) if msg[:token_usage]
|
|
763
804
|
|
|
805
|
+
# Interrupted fan-out anchors its captured subagent trails onto the
|
|
806
|
+
# last message, which is usually this assistant turn (its tool result
|
|
807
|
+
# never got written). Replay them here so they survive a reload.
|
|
808
|
+
replay_subagent_transcript(msg, ui)
|
|
809
|
+
|
|
764
810
|
when "user"
|
|
765
811
|
# Anthropic-format tool results (role: user, content: array of tool_result blocks)
|
|
766
812
|
if msg[:content].is_a?(Array)
|
|
@@ -817,8 +863,12 @@ module Clacky
|
|
|
817
863
|
events = transcript[:events] || transcript["events"] || []
|
|
818
864
|
return if events.empty?
|
|
819
865
|
|
|
866
|
+
skill = transcript[:skill] || transcript["skill"]
|
|
867
|
+
supports_phase = ui.respond_to?(:phase_start) && ui.respond_to?(:phase_end)
|
|
868
|
+
pid = ui.phase_start(kind: "fanout_subagent", label: skill) if supports_phase
|
|
869
|
+
|
|
820
870
|
ui.show_subagent_start(
|
|
821
|
-
skill:
|
|
871
|
+
skill: skill,
|
|
822
872
|
iterations: transcript[:iterations] || transcript["iterations"],
|
|
823
873
|
cost_usd: transcript[:cost_usd] || transcript["cost_usd"]
|
|
824
874
|
)
|
|
@@ -831,7 +881,7 @@ module Clacky
|
|
|
831
881
|
case role
|
|
832
882
|
when "assistant"
|
|
833
883
|
text = extract_text_from_content(content).to_s.strip
|
|
834
|
-
ui.show_assistant_message(text, files: []) unless text.empty?
|
|
884
|
+
ui.show_assistant_message(text, files: [], created_at: ev[:created_at] || ev["created_at"]) unless text.empty?
|
|
835
885
|
Array(tool_calls).each do |tc|
|
|
836
886
|
name = tc[:name] || tc["name"] || ""
|
|
837
887
|
args_raw = tc[:arguments] || tc["arguments"] || {}
|
|
@@ -844,6 +894,7 @@ module Clacky
|
|
|
844
894
|
end
|
|
845
895
|
|
|
846
896
|
ui.show_subagent_end
|
|
897
|
+
ui.phase_end(pid) if supports_phase && pid
|
|
847
898
|
end
|
|
848
899
|
|
|
849
900
|
# Replace the system message in @messages with a freshly built system prompt.
|
|
@@ -928,7 +979,9 @@ module Clacky
|
|
|
928
979
|
|
|
929
980
|
mime_type = (url || "")[/\Adata:([^;]+);/, 1] || "image/jpeg"
|
|
930
981
|
ext = mime_type.split("/").last
|
|
931
|
-
|
|
982
|
+
name = block[:image_name] || block["image_name"]
|
|
983
|
+
name = "image_#{idx + 1}.#{ext}" if name.nil? || name.to_s.strip.empty?
|
|
984
|
+
{ name: name, mime_type: mime_type, data_url: url, path: path }
|
|
932
985
|
end
|
|
933
986
|
end
|
|
934
987
|
|
|
@@ -529,7 +529,15 @@ module Clacky
|
|
|
529
529
|
private def looks_binary?(path)
|
|
530
530
|
return false if path.nil? || !File.exist?(path)
|
|
531
531
|
sample = File.binread(path, 8000)
|
|
532
|
-
sample.include?("\x00")
|
|
532
|
+
return true if sample.include?("\x00")
|
|
533
|
+
sample.force_encoding("UTF-8")
|
|
534
|
+
# The 8000-byte window can cut a multibyte char in half; drop up to
|
|
535
|
+
# three dangling tail bytes before judging the encoding.
|
|
536
|
+
3.times do
|
|
537
|
+
break if sample.valid_encoding?
|
|
538
|
+
sample = sample.byteslice(0, sample.bytesize - 1)
|
|
539
|
+
end
|
|
540
|
+
!sample.valid_encoding?
|
|
533
541
|
rescue StandardError
|
|
534
542
|
true
|
|
535
543
|
end
|
|
@@ -589,6 +597,7 @@ module Clacky
|
|
|
589
597
|
|
|
590
598
|
tasks << {
|
|
591
599
|
task_id: task_id,
|
|
600
|
+
parent_task_id: @task_parents[task_id].to_i,
|
|
592
601
|
summary: summary,
|
|
593
602
|
started_at: meta[:started_at],
|
|
594
603
|
ended_at: meta[:ended_at],
|