openclacky 1.5.18 → 1.5.20
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 +87 -0
- data/README.md +1 -1
- data/README_CN.md +1 -1
- data/README_JA.md +1 -1
- data/docs/session-observation-hooks.md +86 -0
- data/lib/clacky/agent/chunk_index.rb +4 -1
- data/lib/clacky/agent/history_navigation.rb +6 -1
- data/lib/clacky/agent/hook_manager.rb +11 -4
- data/lib/clacky/agent/llm_caller.rb +16 -3
- data/lib/clacky/agent/message_compressor_helper.rb +8 -0
- data/lib/clacky/agent/session_serializer.rb +25 -2
- data/lib/clacky/agent.rb +139 -36
- data/lib/clacky/billing/platform_billing.rb +10 -0
- data/lib/clacky/cli.rb +2 -2
- data/lib/clacky/client.rb +7 -0
- data/lib/clacky/default_extensions/advisor/hooks/advisor.rb +2 -1
- data/lib/clacky/default_extensions/advisor/hooks/advisor_complete.rb +3 -2
- data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +77 -20
- data/lib/clacky/default_extensions/time_machine/panels/time_machine/view.js +1 -0
- data/lib/clacky/default_skills/channel-manager/SKILL.md +70 -9
- data/lib/clacky/default_skills/channel-manager/qq_setup.rb +79 -0
- data/lib/clacky/default_skills/media-gen/SKILL.md +45 -14
- data/lib/clacky/default_skills/onboard/SKILL.md +1 -1
- data/lib/clacky/locales/en.rb +2 -2
- data/lib/clacky/locales/zh.rb +2 -2
- data/lib/clacky/media/openai_compat.rb +89 -2
- data/lib/clacky/message_format/bedrock.rb +5 -2
- data/lib/clacky/message_history.rb +24 -1
- data/lib/clacky/providers.rb +54 -144
- data/lib/clacky/server/channel/adapters/qq/adapter.rb +662 -0
- data/lib/clacky/server/channel/adapters/qq/api_client.rb +245 -0
- data/lib/clacky/server/channel/adapters/qq/gateway_client.rb +340 -0
- data/lib/clacky/server/channel/adapters/qq/qr_binder.rb +140 -0
- data/lib/clacky/server/channel/channel_config.rb +9 -0
- data/lib/clacky/server/channel/channel_ui_controller.rb +6 -2
- data/lib/clacky/server/channel.rb +1 -0
- data/lib/clacky/server/http_server.rb +235 -45
- data/lib/clacky/server/model_prices.rb +14 -3
- data/lib/clacky/server/session_registry.rb +21 -3
- data/lib/clacky/server/web_ui_controller.rb +49 -0
- data/lib/clacky/session_manager.rb +21 -5
- data/lib/clacky/shell_hook_loader.rb +3 -1
- data/lib/clacky/utils/model_pricing.rb +254 -30
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +1133 -308
- data/lib/clacky/web/app.js +82 -13
- data/lib/clacky/web/components/chat-navigator.js +154 -171
- data/lib/clacky/web/components/code-editor.js +6 -2
- data/lib/clacky/web/components/composer.js +11 -2
- data/lib/clacky/web/components/model-picker.js +167 -64
- data/lib/clacky/web/components/sidebar.js +6 -2
- data/lib/clacky/web/core/aside.js +72 -0
- data/lib/clacky/web/features/archive/store.js +182 -0
- data/lib/clacky/web/features/archive/view.js +399 -0
- data/lib/clacky/web/features/backup/view.js +12 -2
- data/lib/clacky/web/features/billing/store.js +1 -1
- data/lib/clacky/web/features/billing/view.js +207 -41
- data/lib/clacky/web/features/channels/store.js +25 -0
- data/lib/clacky/web/features/channels/view.js +168 -5
- data/lib/clacky/web/features/new-session/view.js +1 -0
- data/lib/clacky/web/features/skills/view.js +12 -0
- data/lib/clacky/web/features/trash/store.js +4 -4
- data/lib/clacky/web/features/trash/view.js +57 -9
- data/lib/clacky/web/features/workspace/view.js +509 -28
- data/lib/clacky/web/i18n.js +186 -48
- data/lib/clacky/web/index.html +224 -87
- data/lib/clacky/web/projects.js +131 -3
- data/lib/clacky/web/sessions.js +416 -131
- data/lib/clacky/web/settings.js +62 -24
- data/lib/clacky/web/ws-dispatcher.js +24 -8
- data/lib/clacky.rb +3 -2
- metadata +9 -1
data/lib/clacky/agent.rb
CHANGED
|
@@ -52,6 +52,9 @@ module Clacky
|
|
|
52
52
|
attr_accessor :pinned
|
|
53
53
|
attr_accessor :channel_info
|
|
54
54
|
attr_accessor :project_id
|
|
55
|
+
# ISO8601 stamp set when the user archives the session; nil = active.
|
|
56
|
+
# Archived sessions stay on disk but drop out of every normal list.
|
|
57
|
+
attr_accessor :archived_at
|
|
55
58
|
|
|
56
59
|
REASONING_EFFORTS = %w[low medium high xhigh max].freeze
|
|
57
60
|
MAX_VIDEO_UNDERSTANDING_BYTES = Utils::FileProcessor::MAX_FILE_BYTES
|
|
@@ -93,6 +96,7 @@ module Clacky
|
|
|
93
96
|
@session_id = session_id
|
|
94
97
|
@name = ""
|
|
95
98
|
@pinned = false
|
|
99
|
+
@archived_at = nil
|
|
96
100
|
@history = MessageHistory.new
|
|
97
101
|
@todos = [] # Store todos in memory
|
|
98
102
|
@iterations = 0
|
|
@@ -329,6 +333,14 @@ module Clacky
|
|
|
329
333
|
self
|
|
330
334
|
end
|
|
331
335
|
|
|
336
|
+
# Built-in run failures share the persisted event trail, without exposing
|
|
337
|
+
# built-in event types through the extension API.
|
|
338
|
+
def record_error_event(message, code: nil, top_up_url: nil, raw_message: nil)
|
|
339
|
+
data = { message: message, code: code, top_up_url: top_up_url, raw_message: raw_message }.compact
|
|
340
|
+
@history.append_ext_event({ type: "error", data: data })
|
|
341
|
+
self
|
|
342
|
+
end
|
|
343
|
+
|
|
332
344
|
# Resolve the model name the goal judge should use. Prefer the provider's
|
|
333
345
|
# lite model (cheap/fast for a one-line verdict); fall back to the primary
|
|
334
346
|
# model when no lite is resolvable.
|
|
@@ -485,11 +497,13 @@ module Clacky
|
|
|
485
497
|
@name = new_name.to_s.strip
|
|
486
498
|
end
|
|
487
499
|
|
|
488
|
-
def run(user_input, files: nil, reference_contexts: nil, display_text: nil, created_at: nil, references_display: nil)
|
|
500
|
+
def run(user_input, files: nil, reference_contexts: nil, display_text: nil, created_at: nil, references_display: nil, feedback: nil)
|
|
489
501
|
# Initialized here (not mid-body) because run's rescue/ensure are
|
|
490
502
|
# method-level and must be able to reference them on any exit path.
|
|
491
503
|
result = nil
|
|
492
504
|
run_turn_started = false
|
|
505
|
+
completion_sent = false
|
|
506
|
+
run_context = nil
|
|
493
507
|
|
|
494
508
|
# Intercept /goal ... commands before any task/LLM work. Control-plane
|
|
495
509
|
# commands (status/pause/resume/clear) return immediately without a turn;
|
|
@@ -520,6 +534,10 @@ module Clacky
|
|
|
520
534
|
|
|
521
535
|
# Start new task for Time Machine
|
|
522
536
|
task_id = start_new_task(title: display_text.to_s.empty? ? user_input.to_s : display_text.to_s)
|
|
537
|
+
@feedback = feedback
|
|
538
|
+
run_context = { session_id: @session_id, run_id: task_id.to_s,
|
|
539
|
+
source: @source.to_s, input_created_at: created_at,
|
|
540
|
+
observed_at: Time.now.utc.iso8601(6), status: "running" }
|
|
523
541
|
|
|
524
542
|
# Continuation of a previously-interrupted task (e.g. user sent a
|
|
525
543
|
# supplementary message without stopping the running task) keeps the
|
|
@@ -589,13 +607,15 @@ module Clacky
|
|
|
589
607
|
@input_mutex.synchronize { @accepting_steering = true }
|
|
590
608
|
notify_input_queue
|
|
591
609
|
# The task and user history already exist. A terminal verdict skips the
|
|
592
|
-
# loop
|
|
593
|
-
hook_result = @hooks.trigger(:on_start, user_input)
|
|
610
|
+
# loop; ensure still reports completion and cleans up this started turn.
|
|
611
|
+
hook_result = @hooks.trigger(:on_start, user_input, run_context)
|
|
594
612
|
case hook_result[:action]
|
|
595
613
|
when :deny
|
|
614
|
+
run_context[:reason] = "hook_denied"
|
|
596
615
|
@ui&.show_warning(hook_result[:reason] || "Task denied by hook")
|
|
597
616
|
return result = build_result.merge(queue_paused: true)
|
|
598
617
|
when :handled
|
|
618
|
+
run_context[:handled] = true
|
|
599
619
|
return result = hook_result[:result]
|
|
600
620
|
end
|
|
601
621
|
|
|
@@ -793,6 +813,7 @@ module Clacky
|
|
|
793
813
|
notify_input_queue
|
|
794
814
|
result = build_result(awaiting_user_feedback: awaiting_user_feedback)
|
|
795
815
|
result[:queue_paused] = true if awaiting_user_feedback || task_interrupted
|
|
816
|
+
run_context[:reason] = "tool_denied" if task_interrupted
|
|
796
817
|
|
|
797
818
|
# Run skill evolution hooks after main loop completes
|
|
798
819
|
# Skip if task was interrupted by user (denied tool) or awaiting user feedback
|
|
@@ -825,7 +846,8 @@ module Clacky
|
|
|
825
846
|
awaiting_user_feedback: awaiting_user_feedback
|
|
826
847
|
)
|
|
827
848
|
end
|
|
828
|
-
|
|
849
|
+
completion_sent = true
|
|
850
|
+
complete_run_hook(result, run_context)
|
|
829
851
|
|
|
830
852
|
# Standing-goal loop: after a completed turn, ask the judge whether the
|
|
831
853
|
# goal is met. If not (and budget/health allow), auto-run the next turn
|
|
@@ -840,6 +862,8 @@ module Clacky
|
|
|
840
862
|
result[:queue_paused] = true if @goal_manager&.state&.paused?
|
|
841
863
|
result
|
|
842
864
|
rescue Clacky::AgentInterrupted
|
|
865
|
+
result = { status: :cancelled }
|
|
866
|
+
run_context[:reason] = (Thread.current[:interrupt_reason] || :user).to_s if run_context
|
|
843
867
|
# A cancelled fan-out captured its subagents' progress but never reached
|
|
844
868
|
# observe() to persist it — anchor those trails now so a page reload
|
|
845
869
|
# after the interrupt still shows what the subagents did.
|
|
@@ -852,6 +876,7 @@ module Clacky
|
|
|
852
876
|
# Let CLI handle the interrupt message
|
|
853
877
|
raise
|
|
854
878
|
rescue StandardError => e
|
|
879
|
+
run_context[:error_code] = e.error_code if run_context && e.respond_to?(:error_code)
|
|
855
880
|
# Log complete error information to debug_logs for troubleshooting
|
|
856
881
|
@debug_logs << {
|
|
857
882
|
timestamp: Time.now.iso8601,
|
|
@@ -871,6 +896,11 @@ module Clacky
|
|
|
871
896
|
result = build_result(:error, error: e.message)
|
|
872
897
|
raise
|
|
873
898
|
ensure
|
|
899
|
+
# A recursive goal continuation owns its own completion. Do not let an
|
|
900
|
+
# unwinding predecessor settle a newer turn or report the same turn twice.
|
|
901
|
+
if run_context && !completion_sent && task_id == @current_task_id
|
|
902
|
+
complete_run_hook(result, run_context)
|
|
903
|
+
end
|
|
874
904
|
if run_turn_started && task_id == @current_task_id
|
|
875
905
|
@input_mutex.synchronize do
|
|
876
906
|
@accepting_steering = false
|
|
@@ -888,10 +918,56 @@ module Clacky
|
|
|
888
918
|
Clacky::Telemetry.task!(result: result) if run_turn_started
|
|
889
919
|
end
|
|
890
920
|
|
|
921
|
+
private def complete_run_hook(result, context)
|
|
922
|
+
result = { status: :error } unless result.is_a?(Hash)
|
|
923
|
+
cancelled = context[:reason] || [:cancelled, :interrupted].include?(result[:status])
|
|
924
|
+
status = if cancelled
|
|
925
|
+
"cancelled"
|
|
926
|
+
elsif result[:status] != :success
|
|
927
|
+
"failed"
|
|
928
|
+
elsif result[:awaiting_user_feedback]
|
|
929
|
+
"awaiting_user"
|
|
930
|
+
else
|
|
931
|
+
"completed"
|
|
932
|
+
end
|
|
933
|
+
# Denied work can retain its historical return value for queue control,
|
|
934
|
+
# but completion observers must never interpret it as a successful run.
|
|
935
|
+
notification = cancelled ? result.merge(status: :cancelled) : result
|
|
936
|
+
@hooks.trigger(:on_complete, notification,
|
|
937
|
+
context.merge(status: status, observed_at: Time.now.utc.iso8601(6)))
|
|
938
|
+
end
|
|
939
|
+
|
|
891
940
|
# Shared by initial input and steering; only the execution thread writes history.
|
|
892
941
|
private def append_user_input(user_input, files: nil, reference_contexts: nil,
|
|
893
942
|
display_text: nil, created_at: nil, references_display: nil,
|
|
894
943
|
task_id: @current_task_id)
|
|
944
|
+
# Resolve lightweight metadata first, then commit the accepted user turn
|
|
945
|
+
# before any attachment parser/OCR/sidecar work can block or fail. Session
|
|
946
|
+
# switching and graceful shutdown both read from @history, so the turn
|
|
947
|
+
# must already be authoritative while those slow operations are running.
|
|
948
|
+
skill_command = parse_skill_command(user_input)
|
|
949
|
+
skill_command_display = if skill_command[:found] && skill_command[:skill]
|
|
950
|
+
skill_command[:skill].display_name(Thread.current[:lang])
|
|
951
|
+
end
|
|
952
|
+
created_at ||= Time.now.to_f
|
|
953
|
+
message_id = SecureRandom.uuid
|
|
954
|
+
staged = Array(files).any?
|
|
955
|
+
staged_display_files = display_files_for(files)
|
|
956
|
+
message_metadata = {
|
|
957
|
+
role: "user", message_id: message_id, task_id: task_id, created_at: created_at,
|
|
958
|
+
display_text: display_text,
|
|
959
|
+
skill_command: skill_command[:found] ? skill_command[:skill_name] : nil,
|
|
960
|
+
skill_command_display: skill_command_display,
|
|
961
|
+
display_references: Array(references_display).empty? ? nil : references_display
|
|
962
|
+
}
|
|
963
|
+
if staged
|
|
964
|
+
@history.append(message_metadata.merge(
|
|
965
|
+
content: user_input,
|
|
966
|
+
display_files: staged_display_files.empty? ? nil : staged_display_files
|
|
967
|
+
))
|
|
968
|
+
staged_message_id = message_id
|
|
969
|
+
end
|
|
970
|
+
|
|
895
971
|
# Split files into vision images and disk files; downgrade oversized images to disk
|
|
896
972
|
image_files, disk_files = partition_files(Array(files))
|
|
897
973
|
vision_images, downgraded = resolve_vision_images(image_files)
|
|
@@ -963,37 +1039,21 @@ module Clacky
|
|
|
963
1039
|
# stored here — they recover from image_url blocks in user_content. Downgraded
|
|
964
1040
|
# images (provider has no vision / too large / OCR'd) DO need path here so the
|
|
965
1041
|
# UI can re-render them from the on-disk copy across session switches.
|
|
966
|
-
display_files = all_disk_files
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
1042
|
+
display_files = display_files_for(all_disk_files)
|
|
1043
|
+
resolved_message = message_metadata.merge(
|
|
1044
|
+
content: user_content,
|
|
1045
|
+
display_files: display_files.empty? ? nil : display_files
|
|
1046
|
+
)
|
|
1047
|
+
if staged
|
|
1048
|
+
replaced = @history.replace_user_message(
|
|
1049
|
+
message_id: message_id,
|
|
1050
|
+
message: resolved_message
|
|
1051
|
+
)
|
|
1052
|
+
raise AgentError, "Accepted user message disappeared during attachment processing" unless replaced
|
|
1053
|
+
else
|
|
1054
|
+
@history.append(resolved_message)
|
|
976
1055
|
end
|
|
977
1056
|
|
|
978
|
-
# Resolved once here (not after append) so the user message can carry the
|
|
979
|
-
# confirmed skill name: only a skill that actually dispatches gets marked,
|
|
980
|
-
# so the UI never highlights a typo'd or unavailable command. The display
|
|
981
|
-
# name is resolved against the client's language (Thread.current[:lang],
|
|
982
|
-
# seeded from the WS message / X-Lang header) so the Web UI and third-party
|
|
983
|
-
# clients can render a localized label without re-resolving the skill.
|
|
984
|
-
skill_command = parse_skill_command(user_input)
|
|
985
|
-
skill_command_display = if skill_command[:found] && skill_command[:skill]
|
|
986
|
-
skill_command[:skill].display_name(Thread.current[:lang])
|
|
987
|
-
end
|
|
988
|
-
|
|
989
|
-
created_at ||= Time.now.to_f
|
|
990
|
-
@history.append({ role: "user", content: user_content, task_id: task_id, created_at: created_at,
|
|
991
|
-
display_text: display_text,
|
|
992
|
-
skill_command: skill_command[:found] ? skill_command[:skill_name] : nil,
|
|
993
|
-
skill_command_display: skill_command_display,
|
|
994
|
-
display_files: display_files.empty? ? nil : display_files,
|
|
995
|
-
display_references: Array(references_display).empty? ? nil : references_display })
|
|
996
|
-
|
|
997
1057
|
# Inject disk file references as a system_injected message so:
|
|
998
1058
|
# - LLM sees the file info (system_injected is NOT stripped from to_api)
|
|
999
1059
|
# - replay_history skips it (next if ev[:system_injected]), keeping the user bubble clean
|
|
@@ -1108,6 +1168,36 @@ module Clacky
|
|
|
1108
1168
|
# skill-declared model not found) still stops the progress spinner.
|
|
1109
1169
|
inject_skill_command_as_assistant_message(skill_command, task_id)
|
|
1110
1170
|
|
|
1171
|
+
rescue Clacky::AgentInterrupted, StandardError
|
|
1172
|
+
# The staged turn exists only to make an accepted upload replayable while
|
|
1173
|
+
# attachment work is in progress. If that work crashes, remove this turn
|
|
1174
|
+
# and anything injected after it so the next request cannot inherit a
|
|
1175
|
+
# half-resolved attachment context. Parser failures returned as FileRef
|
|
1176
|
+
# metadata do not raise and therefore remain visible/actionable.
|
|
1177
|
+
@history.truncate_from_message(staged_message_id) if staged_message_id
|
|
1178
|
+
raise
|
|
1179
|
+
end
|
|
1180
|
+
|
|
1181
|
+
# Build replay-only attachment metadata without parsing file contents.
|
|
1182
|
+
# @mention references render through display_references and must not also
|
|
1183
|
+
# appear as plain attachment badges.
|
|
1184
|
+
private def display_files_for(files)
|
|
1185
|
+
Array(files).filter_map do |file|
|
|
1186
|
+
next if file[:reference] || file["reference"]
|
|
1187
|
+
|
|
1188
|
+
name = file[:name] || file["name"]
|
|
1189
|
+
next unless name
|
|
1190
|
+
|
|
1191
|
+
mime_type = file[:mime_type] || file["mime_type"]
|
|
1192
|
+
type = file[:type] || file["type"]
|
|
1193
|
+
type ||= "image" if mime_type.to_s.start_with?("image/")
|
|
1194
|
+
{
|
|
1195
|
+
name: name,
|
|
1196
|
+
type: type || "file",
|
|
1197
|
+
path: file[:path] || file["path"],
|
|
1198
|
+
preview_path: file[:preview_path] || file["preview_path"]
|
|
1199
|
+
}
|
|
1200
|
+
end
|
|
1111
1201
|
end
|
|
1112
1202
|
|
|
1113
1203
|
def enqueue_input(content, delivery: :queue, **options)
|
|
@@ -1240,6 +1330,7 @@ module Clacky
|
|
|
1240
1330
|
end
|
|
1241
1331
|
entries.each do |entry|
|
|
1242
1332
|
options = entry[:options].dup
|
|
1333
|
+
options.delete(:feedback)
|
|
1243
1334
|
source = options.delete(:source) || :web
|
|
1244
1335
|
history_size = @history.size
|
|
1245
1336
|
begin
|
|
@@ -1655,9 +1746,20 @@ module Clacky
|
|
|
1655
1746
|
# can extract the questions and emit a "request_feedback" event
|
|
1656
1747
|
# (renders as a clickable card in the browser).
|
|
1657
1748
|
# Fallback UIs (terminal, IM channels) receive the formatted text message.
|
|
1658
|
-
@ui
|
|
1659
|
-
|
|
1660
|
-
|
|
1749
|
+
if @feedback && @ui.respond_to?(:wait_for_feedback)
|
|
1750
|
+
questions = Tools::AskUser.normalize_questions(args)
|
|
1751
|
+
question = questions.first
|
|
1752
|
+
label = if questions.one? && !question[:multi] && question[:recommended]
|
|
1753
|
+
question[:options][question[:recommended]]
|
|
1754
|
+
end
|
|
1755
|
+
answer, feedback_options = @ui.wait_for_feedback(timeout_seconds: @feedback[:timeout_seconds], question: question[:question], recommended_label: label) do
|
|
1756
|
+
@ui.show_tool_call(call[:name], call[:arguments])
|
|
1757
|
+
end
|
|
1758
|
+
check_stale!
|
|
1759
|
+
result[:awaiting_feedback] = awaiting_feedback = !answer
|
|
1760
|
+
enqueue_input("Answer: #{answer}", delivery: :steer, created_at: Time.now.to_f, **(feedback_options || {})) if answer
|
|
1761
|
+
elsif @config.permission_mode == :auto_approve
|
|
1762
|
+
@ui&.show_tool_call(call[:name], call[:arguments])
|
|
1661
1763
|
# auto_approve means the agent runs unattended by default, but a
|
|
1662
1764
|
# human MAY be watching the terminal. Show a short interactive
|
|
1663
1765
|
# countdown: if the user steps in, hand control over and wait for
|
|
@@ -1680,6 +1782,7 @@ module Clacky
|
|
|
1680
1782
|
end
|
|
1681
1783
|
else
|
|
1682
1784
|
# confirm_all / confirm_safes — a human is present, truly wait for user input.
|
|
1785
|
+
@ui&.show_tool_call(call[:name], call[:arguments])
|
|
1683
1786
|
awaiting_feedback = true
|
|
1684
1787
|
end
|
|
1685
1788
|
else
|
|
@@ -24,6 +24,7 @@ module Clacky
|
|
|
24
24
|
"dsk-deepseek-v4-flash-vision-exp" => "deepseek-v4-flash-vision-exp",
|
|
25
25
|
# gpt via bedrock (abs-, OpenAI-compatible endpoint)
|
|
26
26
|
"abs-gpt-6-astra" => "global.openai.gpt-6-astra",
|
|
27
|
+
"abs-gpt-6.1-sol" => "global.openai.gpt-6.1-sol",
|
|
27
28
|
"abs-gpt-6-sol" => "global.openai.gpt-6-sol",
|
|
28
29
|
"abs-gpt-6-luna" => "global.openai.gpt-6-luna",
|
|
29
30
|
"abs-gpt-5.6-sol" => "global.openai.gpt-5.6-sol",
|
|
@@ -37,11 +38,20 @@ module Clacky
|
|
|
37
38
|
"abs-claude-opus-4-8" => "global.anthropic.claude-opus-4-8",
|
|
38
39
|
"abs-claude-opus-4-7" => "global.anthropic.claude-opus-4-7",
|
|
39
40
|
"abs-claude-opus-4-6" => "global.anthropic.claude-opus-4-6",
|
|
41
|
+
"abs-claude-sonnet-5-5" => "global.anthropic.claude-sonnet-5-5",
|
|
40
42
|
"abs-claude-sonnet-5" => "global.anthropic.claude-sonnet-5",
|
|
41
43
|
"abs-claude-sonnet-4-6" => "global.anthropic.claude-sonnet-4-6",
|
|
42
44
|
"abs-claude-sonnet-4-5" => "global.anthropic.claude-sonnet-4-5",
|
|
45
|
+
"abs-claude-haiku-5-5" => "global.anthropic.claude-haiku-5-5",
|
|
43
46
|
"abs-claude-haiku-4-5" => "global.anthropic.claude-haiku-4-5",
|
|
47
|
+
# GLM / Kimi / Grok via TokHub (oc-). The gateway books these under the
|
|
48
|
+
# alias itself, so the mapping is an identity — it exists so the billing UI
|
|
49
|
+
# can resolve the alias instead of dropping the record as unknown.
|
|
50
|
+
"oc-glm-5.3" => "oc-glm-5.3",
|
|
51
|
+
"oc-kimi-k3" => "oc-kimi-k3",
|
|
52
|
+
"oc-grok-4.7" => "oc-grok-4.7",
|
|
44
53
|
# gemini chat (or-)
|
|
54
|
+
"or-gemini-4-argon" => "gemini-4-argon",
|
|
45
55
|
"or-gemini-3-1-pro" => "gemini-3.1-pro-preview",
|
|
46
56
|
"or-gemini-3-8-flash" => "gemini-3.8-flash",
|
|
47
57
|
"or-gemini-3-7-flash" => "gemini-3.7-flash",
|
data/lib/clacky/cli.rb
CHANGED
|
@@ -481,8 +481,8 @@ module Clacky
|
|
|
481
481
|
private def auto_name_session(agent, input)
|
|
482
482
|
return unless agent.history.empty?
|
|
483
483
|
|
|
484
|
-
auto_name = input.to_s.gsub(/\s+/, " ").strip[0,
|
|
485
|
-
auto_name += "…" if input.to_s.strip.length >
|
|
484
|
+
auto_name = input.to_s.gsub(/\s+/, " ").strip[0, 80]
|
|
485
|
+
auto_name += "…" if input.to_s.strip.length > 80
|
|
486
486
|
agent.rename(auto_name)
|
|
487
487
|
end
|
|
488
488
|
|
data/lib/clacky/client.rb
CHANGED
|
@@ -56,6 +56,13 @@ module Clacky
|
|
|
56
56
|
@read_timeout = read_timeout
|
|
57
57
|
end
|
|
58
58
|
|
|
59
|
+
# Console URL where the user can inspect credit / API key state. nil when
|
|
60
|
+
# the provider has no preset (custom base_url).
|
|
61
|
+
def account_url
|
|
62
|
+
preset = Providers.get(@provider_id)
|
|
63
|
+
preset && preset["website_url"]
|
|
64
|
+
end
|
|
65
|
+
|
|
59
66
|
# Returns true when the client is using the AWS Bedrock Converse API.
|
|
60
67
|
def bedrock?
|
|
61
68
|
@use_bedrock
|
|
@@ -127,7 +127,7 @@ module Clacky
|
|
|
127
127
|
# rounds with no tool calls at all (e.g. the very first "hi"). The
|
|
128
128
|
# pending event is emitted synchronously so the UI can show a "working"
|
|
129
129
|
# state immediately instead of the card popping in out of nowhere.
|
|
130
|
-
def finish_run
|
|
130
|
+
def finish_run(analyze: true)
|
|
131
131
|
snapshot = @mutex.synchronize do
|
|
132
132
|
snap = {
|
|
133
133
|
tools: @tools_this_run,
|
|
@@ -139,6 +139,7 @@ module Clacky
|
|
|
139
139
|
@tools_this_run = 0
|
|
140
140
|
snap
|
|
141
141
|
end
|
|
142
|
+
return unless analyze
|
|
142
143
|
Clacky::Logger.info("[Advisor] finish_run",
|
|
143
144
|
session: @agent.session_id.to_s,
|
|
144
145
|
tools: snapshot[:tools],
|
|
@@ -4,8 +4,9 @@ require_relative "advisor"
|
|
|
4
4
|
|
|
5
5
|
# on_complete: a run has ended — reset the per-run analysis budget so the
|
|
6
6
|
# next run starts fresh.
|
|
7
|
-
Clacky::ExtensionHookRegistry.add do |
|
|
7
|
+
Clacky::ExtensionHookRegistry.add do |result, agent, context|
|
|
8
8
|
next unless Clacky::Advisor.enabled_for?(agent)
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
successful = result.is_a?(Hash) && result[:status] == :success && !context&.dig(:handled)
|
|
11
|
+
Clacky::Advisor.worker_for(agent).finish_run(analyze: successful)
|
|
11
12
|
end
|
|
@@ -112,6 +112,12 @@
|
|
|
112
112
|
"pub.screenshot.err": "Upload failed: {{msg}}",
|
|
113
113
|
"pub.screenshot.err.type": "Only PNG / JPG / GIF are allowed",
|
|
114
114
|
"pub.screenshot.err.size": "File size must be under 5 MB",
|
|
115
|
+
"pub.screenshot.err.unbound": "this device isn't linked to a platform account",
|
|
116
|
+
"pub.screenshot.err.missingFile": "no file received",
|
|
117
|
+
"pub.screenshot.err.extId": "missing extension ID",
|
|
118
|
+
"pub.screenshot.err.service": "the upload service is unavailable, please try again later",
|
|
119
|
+
"pub.screenshot.err.generic": "Upload failed, please try again",
|
|
120
|
+
"pub.screenshot.bind": "Authorize",
|
|
115
121
|
"pub.readme.edit.title": "Edit README",
|
|
116
122
|
"readme.pane.editor": "Markdown editor",
|
|
117
123
|
"readme.pane.preview": "Preview",
|
|
@@ -318,6 +324,12 @@
|
|
|
318
324
|
"pub.screenshot.err": "上传失败:{{msg}}",
|
|
319
325
|
"pub.screenshot.err.type": "仅支持 PNG / JPG / GIF 格式",
|
|
320
326
|
"pub.screenshot.err.size": "文件大小不能超过 5 MB",
|
|
327
|
+
"pub.screenshot.err.unbound": "设备未绑定平台账号",
|
|
328
|
+
"pub.screenshot.err.missingFile": "未收到文件",
|
|
329
|
+
"pub.screenshot.err.extId": "缺少扩展 ID",
|
|
330
|
+
"pub.screenshot.err.service": "上传服务暂不可用,请稍后重试",
|
|
331
|
+
"pub.screenshot.err.generic": "上传失败,请重试",
|
|
332
|
+
"pub.screenshot.bind": "去授权",
|
|
321
333
|
"pub.readme.edit.title": "编辑使用说明",
|
|
322
334
|
"readme.pane.editor": "Markdown 编写",
|
|
323
335
|
"readme.pane.preview": "预览",
|
|
@@ -651,15 +663,17 @@
|
|
|
651
663
|
|
|
652
664
|
const status = el("p", { class: "studio-modal-status", text: t("bind.body") });
|
|
653
665
|
const codeLine = el("p", { class: "studio-modal-code" });
|
|
654
|
-
const link = el("a", { class: "
|
|
666
|
+
const link = el("a", { class: "btn-primary", target: "_blank", rel: "noopener", text: t("bind.openLink") });
|
|
655
667
|
link.style.display = "none";
|
|
656
668
|
|
|
657
669
|
const modal = openModal({
|
|
658
670
|
title: t("bind.title"),
|
|
659
|
-
body: el("div", null, [status, codeLine
|
|
671
|
+
body: el("div", null, [status, codeLine]),
|
|
660
672
|
onClose: () => { polling = false; resolve(false); },
|
|
661
673
|
buttons: [{ label: t("bind.cancel"), keepOpen: false, onClick: () => { polling = false; resolve(false); } }],
|
|
662
674
|
});
|
|
675
|
+
// A real link (not a button) so it still opens when the browser blocks our popup.
|
|
676
|
+
modal.footer.appendChild(link);
|
|
663
677
|
|
|
664
678
|
function finish(ok) { polling = false; modal.close(); resolve(ok); }
|
|
665
679
|
|
|
@@ -1342,21 +1356,38 @@
|
|
|
1342
1356
|
|
|
1343
1357
|
const screenshotErr = el("p", { class: "studio-modal-status studio-modal-status-error", style: "display:none;margin-top:6px" });
|
|
1344
1358
|
screenshotsSection.appendChild(screenshotErr);
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
screenshotErr.
|
|
1350
|
-
if (
|
|
1351
|
-
screenshotErr.
|
|
1352
|
-
screenshotErr.
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
return;
|
|
1359
|
+
|
|
1360
|
+
// Shows a localized error; passing retryFile appends an "Authorize" link that
|
|
1361
|
+
// reopens device binding and re-uploads that file once bound.
|
|
1362
|
+
function showScreenshotError(text, retryFile) {
|
|
1363
|
+
screenshotErr.textContent = text;
|
|
1364
|
+
if (retryFile) {
|
|
1365
|
+
screenshotErr.appendChild(document.createTextNode(" "));
|
|
1366
|
+
screenshotErr.appendChild(el("button", {
|
|
1367
|
+
type: "button",
|
|
1368
|
+
class: "studio-link-btn",
|
|
1369
|
+
text: t("pub.screenshot.bind"),
|
|
1370
|
+
onclick: () => bindThenUploadScreenshot(retryFile),
|
|
1371
|
+
}));
|
|
1359
1372
|
}
|
|
1373
|
+
screenshotErr.style.display = "";
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
function screenshotErrorText(e) {
|
|
1377
|
+
if (e.status === 422 && e.message === "missing file") return t("pub.screenshot.err", { msg: t("pub.screenshot.err.missingFile") });
|
|
1378
|
+
if (e.status === 422 && e.message === "ext_id required") return t("pub.screenshot.err", { msg: t("pub.screenshot.err.extId") });
|
|
1379
|
+
if (e.status === 502) return t("pub.screenshot.err", { msg: t("pub.screenshot.err.service") });
|
|
1380
|
+
return t("pub.screenshot.err.generic");
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
async function bindThenUploadScreenshot(file) {
|
|
1384
|
+
screenshotErr.style.display = "none";
|
|
1385
|
+
if (await runBindingFlow()) return uploadScreenshot(file);
|
|
1386
|
+
showScreenshotError(t("pub.screenshot.err", { msg: t("pub.screenshot.err.unbound") }), file);
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
async function uploadScreenshot(file) {
|
|
1390
|
+
let needsBinding = false;
|
|
1360
1391
|
uploadBtn.disabled = true;
|
|
1361
1392
|
uploadBtn.textContent = t("pub.screenshot.uploading");
|
|
1362
1393
|
screenshotErr.style.display = "none";
|
|
@@ -1366,7 +1397,12 @@
|
|
|
1366
1397
|
form.append("file", file);
|
|
1367
1398
|
const res = await fetch("/api/ext/ext-studio/screenshot", { method: "POST", body: form });
|
|
1368
1399
|
const json = await res.json();
|
|
1369
|
-
if (!res.ok || !json.ok)
|
|
1400
|
+
if (!res.ok || !json.ok) {
|
|
1401
|
+
const err = new Error(json.error || `Request failed (${res.status})`);
|
|
1402
|
+
err.status = res.status;
|
|
1403
|
+
err.data = json;
|
|
1404
|
+
throw err;
|
|
1405
|
+
}
|
|
1370
1406
|
// Insert  at cursor position in the editor.
|
|
1371
1407
|
// Ensure the image tag is on its own line by adding newlines if needed.
|
|
1372
1408
|
const mdImg = ``;
|
|
@@ -1382,12 +1418,33 @@
|
|
|
1382
1418
|
readmeArea.focus();
|
|
1383
1419
|
readmeArea.dispatchEvent(new Event("input"));
|
|
1384
1420
|
} catch (e) {
|
|
1385
|
-
|
|
1386
|
-
|
|
1421
|
+
if (e.status === 428 && e.data && e.data.needs_binding) {
|
|
1422
|
+
needsBinding = true;
|
|
1423
|
+
} else {
|
|
1424
|
+
console.error("[ext-studio] screenshot upload failed:", e);
|
|
1425
|
+
showScreenshotError(screenshotErrorText(e));
|
|
1426
|
+
}
|
|
1387
1427
|
} finally {
|
|
1388
1428
|
uploadBtn.disabled = false;
|
|
1389
1429
|
uploadBtn.textContent = t("pub.screenshot.upload");
|
|
1390
1430
|
}
|
|
1431
|
+
if (needsBinding) await bindThenUploadScreenshot(file);
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
fileInput.addEventListener("change", () => {
|
|
1435
|
+
const file = fileInput.files[0];
|
|
1436
|
+
if (!file) return;
|
|
1437
|
+
fileInput.value = "";
|
|
1438
|
+
screenshotErr.style.display = "none";
|
|
1439
|
+
if (!["image/png", "image/jpeg", "image/gif"].includes(file.type)) {
|
|
1440
|
+
showScreenshotError(t("pub.screenshot.err", { msg: t("pub.screenshot.err.type") }));
|
|
1441
|
+
return;
|
|
1442
|
+
}
|
|
1443
|
+
if (file.size > 5 * 1024 * 1024) {
|
|
1444
|
+
showScreenshotError(t("pub.screenshot.err", { msg: t("pub.screenshot.err.size") }));
|
|
1445
|
+
return;
|
|
1446
|
+
}
|
|
1447
|
+
uploadScreenshot(file);
|
|
1391
1448
|
});
|
|
1392
1449
|
|
|
1393
1450
|
box.appendChild(screenshotsSection);
|
|
@@ -2312,7 +2369,7 @@
|
|
|
2312
2369
|
.studio-input:focus { border-color: var(--color-accent-primary); outline: none; }
|
|
2313
2370
|
.studio-input-error { border-color: var(--color-error) !important; }
|
|
2314
2371
|
.studio-modal-code { margin: 0 0 8px; font-size: 13px; font-family: monospace; color: var(--color-text-primary); }
|
|
2315
|
-
.studio-modal-
|
|
2372
|
+
.studio-modal .modal-footer a.btn-primary { display: inline-flex; align-items: center; font-size: 14px; text-decoration: none; }
|
|
2316
2373
|
.studio-pub-success { margin-right: auto; font-size: 13px; font-weight: 500; color: var(--color-success); }
|
|
2317
2374
|
.studio-section { margin-bottom: 20px; }
|
|
2318
2375
|
.studio-detail-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 6px; }
|
|
@@ -392,6 +392,7 @@
|
|
|
392
392
|
});
|
|
393
393
|
if (res.ok) {
|
|
394
394
|
state.footEl.textContent = res.message || t("tm.restored", "已恢复");
|
|
395
|
+
if (Clacky.Workspace) Clacky.Workspace.notifyTaskCompleted(state.sessionId);
|
|
395
396
|
state.expanded = null;
|
|
396
397
|
closeDetail(state);
|
|
397
398
|
await loadHistory(state);
|