openclacky 0.9.38 → 1.0.0.beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.clacky/skills/gem-release/SKILL.md +67 -13
- data/CHANGELOG.md +40 -0
- data/lib/clacky/agent/llm_caller.rb +48 -2
- data/lib/clacky/agent/memory_updater.rb +131 -35
- data/lib/clacky/agent/message_compressor.rb +30 -3
- data/lib/clacky/agent/message_compressor_helper.rb +53 -19
- data/lib/clacky/agent/time_machine.rb +12 -3
- data/lib/clacky/agent/tool_executor.rb +0 -3
- data/lib/clacky/agent.rb +190 -61
- data/lib/clacky/agent_config.rb +201 -47
- data/lib/clacky/brand_config.rb +77 -5
- data/lib/clacky/cli.rb +101 -45
- data/lib/clacky/message_format/bedrock.rb +4 -0
- data/lib/clacky/message_history.rb +79 -4
- data/lib/clacky/platform_http_client.rb +7 -7
- data/lib/clacky/providers.rb +170 -8
- data/lib/clacky/server/http_server.rb +138 -21
- data/lib/clacky/telemetry.rb +111 -0
- data/lib/clacky/tools/terminal.rb +27 -0
- data/lib/clacky/tools/todo_manager.rb +11 -2
- data/lib/clacky/ui2/layout_manager.rb +22 -1
- data/lib/clacky/ui2/progress_handle.rb +291 -0
- data/lib/clacky/ui2/ui_controller.rb +261 -185
- data/lib/clacky/ui_interface.rb +69 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +53 -0
- data/lib/clacky/web/app.js +1 -1
- data/lib/clacky/web/brand.js +112 -1
- data/lib/clacky/web/i18n.js +24 -16
- data/lib/clacky/web/index.html +15 -2
- data/lib/clacky/web/sessions.js +23 -6
- data/lib/clacky/web/settings.js +34 -0
- data/lib/clacky/web/ws.js +3 -2
- data/lib/clacky.rb +1 -0
- data/scripts/install.ps1 +20 -5
- metadata +3 -2
- data/lib/clacky/ui2/README.md +0 -214
|
@@ -93,13 +93,22 @@ module Clacky
|
|
|
93
93
|
# Filter messages to only show tasks up to active_task_id.
|
|
94
94
|
# This hides "future" messages when user has undone.
|
|
95
95
|
# Returns API-ready array (strips internal fields + handles orphaned tool_calls).
|
|
96
|
+
# @param force_reasoning_content_pad [Boolean] forwarded to MessageHistory,
|
|
97
|
+
# enables one-shot pad-and-retry for thinking-mode providers that
|
|
98
|
+
# require reasoning_content on every assistant message.
|
|
96
99
|
# Made public for testing
|
|
97
|
-
def active_messages
|
|
98
|
-
|
|
100
|
+
def active_messages(force_reasoning_content_pad: false)
|
|
101
|
+
if @active_task_id == @current_task_id
|
|
102
|
+
return @history.to_api(force_reasoning_content_pad: force_reasoning_content_pad)
|
|
103
|
+
end
|
|
99
104
|
|
|
100
|
-
@history.for_task(@active_task_id).map do |msg|
|
|
105
|
+
stripped = @history.for_task(@active_task_id).map do |msg|
|
|
101
106
|
msg.reject { |k, _| MessageHistory::INTERNAL_FIELDS.include?(k) }
|
|
102
107
|
end
|
|
108
|
+
# Apply the same reasoning_content padding rule used by to_api so
|
|
109
|
+
# Time Machine replays satisfy thinking-mode providers after a
|
|
110
|
+
# 400 retry.
|
|
111
|
+
MessageHistory.pad_reasoning_content_if_needed(stripped, force: force_reasoning_content_pad)
|
|
103
112
|
end
|
|
104
113
|
|
|
105
114
|
# Undo to parent task
|
|
@@ -10,9 +10,6 @@ module Clacky
|
|
|
10
10
|
# @param tool_params [Hash, String] Tool parameters
|
|
11
11
|
# @return [Boolean] true if should auto-execute
|
|
12
12
|
def should_auto_execute?(tool_name, tool_params = {})
|
|
13
|
-
# During memory update phase, always auto-execute (no user confirmation needed)
|
|
14
|
-
return true if @memory_updating
|
|
15
|
-
|
|
16
13
|
case @config.permission_mode
|
|
17
14
|
when :auto_approve, :confirm_all
|
|
18
15
|
# Both modes auto-execute all file/shell tools without confirmation.
|
data/lib/clacky/agent.rb
CHANGED
|
@@ -265,9 +265,15 @@ module Clacky
|
|
|
265
265
|
path = f[:path] || f["path"]
|
|
266
266
|
name = f[:name] || f["name"]
|
|
267
267
|
next f unless path && File.exist?(path.to_s)
|
|
268
|
+
# Preserve the downgrade_reason tag across the remap (process_path
|
|
269
|
+
# returns a fresh FileRef that doesn't know about it). Without this,
|
|
270
|
+
# the file_prompt builder can't emit the "not supported by model" /
|
|
271
|
+
# "too large" note for downgraded images.
|
|
272
|
+
downgrade_reason = f[:downgrade_reason] || f["downgrade_reason"]
|
|
268
273
|
ref = Utils::FileProcessor.process_path(path, name: name)
|
|
269
274
|
{ name: ref.name, type: ref.type.to_s, path: ref.original_path,
|
|
270
|
-
preview_path: ref.preview_path, parse_error: ref.parse_error, parser_path: ref.parser_path
|
|
275
|
+
preview_path: ref.preview_path, parse_error: ref.parse_error, parser_path: ref.parser_path,
|
|
276
|
+
downgrade_reason: downgrade_reason }
|
|
271
277
|
end
|
|
272
278
|
|
|
273
279
|
# Build display_files for replay: lightweight metadata so the UI can reconstruct
|
|
@@ -295,13 +301,14 @@ module Clacky
|
|
|
295
301
|
|
|
296
302
|
unless all_meta_files.empty?
|
|
297
303
|
file_prompt = all_meta_files.filter_map do |f|
|
|
298
|
-
name
|
|
299
|
-
type
|
|
300
|
-
path
|
|
301
|
-
preview_path
|
|
302
|
-
size_bytes
|
|
303
|
-
parse_error
|
|
304
|
-
parser_path
|
|
304
|
+
name = f[:name] || f["name"]
|
|
305
|
+
type = f[:type] || f["type"]
|
|
306
|
+
path = f[:path] || f["path"]
|
|
307
|
+
preview_path = f[:preview_path] || f["preview_path"]
|
|
308
|
+
size_bytes = f[:size_bytes] || f["size_bytes"]
|
|
309
|
+
parse_error = f[:parse_error] || f["parse_error"]
|
|
310
|
+
parser_path = f[:parser_path] || f["parser_path"]
|
|
311
|
+
downgrade_reason = f[:downgrade_reason] || f["downgrade_reason"]
|
|
305
312
|
|
|
306
313
|
next unless name
|
|
307
314
|
|
|
@@ -310,6 +317,13 @@ module Clacky
|
|
|
310
317
|
lines << "Original: #{path}" if path
|
|
311
318
|
lines << "Preview (Markdown): #{preview_path}" if preview_path
|
|
312
319
|
|
|
320
|
+
# Inline note explaining why an image was *not* sent as vision
|
|
321
|
+
# content. Colocated with the file info (not in system prompt) so
|
|
322
|
+
# it reflects the exact reason for *this* upload under *this*
|
|
323
|
+
# model — switching models later won't leave stale warnings.
|
|
324
|
+
note = downgrade_note_for(downgrade_reason)
|
|
325
|
+
lines << "Note: #{note}" if note
|
|
326
|
+
|
|
313
327
|
# Parser failed — instruct LLM to fix and re-run
|
|
314
328
|
if preview_path.nil? && parse_error
|
|
315
329
|
lines << "Parse failed: #{parse_error}"
|
|
@@ -362,10 +376,7 @@ module Clacky
|
|
|
362
376
|
|
|
363
377
|
# Check if done (no more tool calls needed)
|
|
364
378
|
if response[:finish_reason] == "stop" || response[:tool_calls].nil? || response[:tool_calls].empty?
|
|
365
|
-
|
|
366
|
-
if @memory_updating && response[:content] && !response[:content].empty?
|
|
367
|
-
@ui&.show_info(response[:content].strip)
|
|
368
|
-
elsif response[:content] && !response[:content].empty?
|
|
379
|
+
if response[:content] && !response[:content].empty?
|
|
369
380
|
emit_assistant_message(response[:content])
|
|
370
381
|
end
|
|
371
382
|
|
|
@@ -382,15 +393,11 @@ module Clacky
|
|
|
382
393
|
end
|
|
383
394
|
end
|
|
384
395
|
|
|
385
|
-
# Inject memory update prompt and let the loop handle it naturally
|
|
386
|
-
next if inject_memory_prompt!
|
|
387
|
-
|
|
388
396
|
break
|
|
389
397
|
end
|
|
390
398
|
|
|
391
399
|
# Show assistant message if there's content before tool calls
|
|
392
|
-
|
|
393
|
-
if response[:content] && !response[:content].empty? && !@memory_updating
|
|
400
|
+
if response[:content] && !response[:content].empty?
|
|
394
401
|
emit_assistant_message(response[:content])
|
|
395
402
|
end
|
|
396
403
|
|
|
@@ -453,6 +460,17 @@ module Clacky
|
|
|
453
460
|
run_skill_evolution_hooks
|
|
454
461
|
end
|
|
455
462
|
|
|
463
|
+
# Run long-term memory update as a forked subagent BEFORE we print
|
|
464
|
+
# show_complete. Running it as a subagent (rather than inline in
|
|
465
|
+
# the main loop) gives us correct visual ordering structurally:
|
|
466
|
+
# the subagent blocks until done, its progress spinner finishes,
|
|
467
|
+
# and only then [OK] Task Complete is printed. No cleanup dance,
|
|
468
|
+
# no cross-method progress handle holding.
|
|
469
|
+
# Skip on interrupt / feedback / subagent (self-guarded inside too).
|
|
470
|
+
unless @is_subagent || task_interrupted || awaiting_user_feedback
|
|
471
|
+
run_memory_update_subagent
|
|
472
|
+
end
|
|
473
|
+
|
|
456
474
|
if @is_subagent
|
|
457
475
|
# Parent agent (skill_manager) prints the completion summary; skip here.
|
|
458
476
|
else
|
|
@@ -489,9 +507,6 @@ module Clacky
|
|
|
489
507
|
result = build_result(:error, error: e.message) # rubocop:disable Lint/UselessAssignment
|
|
490
508
|
raise
|
|
491
509
|
ensure
|
|
492
|
-
# Always clean up memory update messages, even if interrupted or error occurred
|
|
493
|
-
cleanup_memory_messages
|
|
494
|
-
|
|
495
510
|
# Safety net: ensure any lingering progress spinner is stopped.
|
|
496
511
|
# Normal paths close their own spinners; this guards against exceptions
|
|
497
512
|
# raised between a progress slot's active/done pair.
|
|
@@ -501,6 +516,10 @@ module Clacky
|
|
|
501
516
|
# This covers the inline-injection path; the subagent path shreds immediately after
|
|
502
517
|
# subagent.run returns (see execute_skill_with_subagent).
|
|
503
518
|
shred_script_tmpdirs
|
|
519
|
+
|
|
520
|
+
# Fire-and-forget telemetry after every agent run.
|
|
521
|
+
# Tracks daily active users (distinct devices per day) and task volume.
|
|
522
|
+
Clacky::Telemetry.task!
|
|
504
523
|
end
|
|
505
524
|
end
|
|
506
525
|
|
|
@@ -526,21 +545,33 @@ module Clacky
|
|
|
526
545
|
@ui&.show_info(
|
|
527
546
|
"Message history compression starting (~#{compression_context[:original_token_count]} tokens, #{compression_context[:original_message_count]} messages) - Level #{compression_context[:compression_level]}"
|
|
528
547
|
)
|
|
529
|
-
|
|
530
|
-
# handle_compression_response will close it with the summary line on
|
|
531
|
-
# success; on failure the outer ensure below finalizes it.
|
|
532
|
-
@ui&.show_progress(
|
|
533
|
-
"Compressing message history...",
|
|
534
|
-
progress_type: "idle_compress",
|
|
535
|
-
phase: "active"
|
|
536
|
-
)
|
|
548
|
+
|
|
537
549
|
compression_message = compression_context[:compression_message]
|
|
538
550
|
@history.append(compression_message)
|
|
539
551
|
compression_handled = false
|
|
552
|
+
|
|
553
|
+
# Open a dedicated quiet-style handle for the compression work.
|
|
554
|
+
# This sits on top of the outer thinking progress (if any); Plan B
|
|
555
|
+
# semantics detach the outer spinner until we finish here. On any
|
|
556
|
+
# exception the ensure block in with_progress guarantees the
|
|
557
|
+
# handle is released — no more orphan gray ticker colliding with
|
|
558
|
+
# a yellow ticker (the original flicker bug).
|
|
559
|
+
#
|
|
560
|
+
# NOTE: safe-navigation (+&.+) with blocks silently skips the
|
|
561
|
+
# block when the receiver is nil. We need the compression work to
|
|
562
|
+
# run even when @ui is nil (e.g. in tests), so branch explicitly.
|
|
540
563
|
begin
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
564
|
+
if @ui
|
|
565
|
+
@ui.with_progress(message: "Compressing message history...", style: :quiet) do |handle|
|
|
566
|
+
response = call_llm
|
|
567
|
+
handle_compression_response(response, compression_context, progress: handle)
|
|
568
|
+
compression_handled = true
|
|
569
|
+
end
|
|
570
|
+
else
|
|
571
|
+
response = call_llm
|
|
572
|
+
handle_compression_response(response, compression_context)
|
|
573
|
+
compression_handled = true
|
|
574
|
+
end
|
|
544
575
|
ensure
|
|
545
576
|
# If interrupted or failed, roll back the speculative compression message
|
|
546
577
|
# so it doesn't pollute future conversation turns.
|
|
@@ -552,8 +583,6 @@ module Clacky
|
|
|
552
583
|
# (with the user's new message as the last entry), producing consecutive user messages
|
|
553
584
|
# that confuse the LLM into echoing compression instructions.
|
|
554
585
|
@compression_level -= 1
|
|
555
|
-
# Close the compression progress slot so the spinner does not linger.
|
|
556
|
-
@ui&.show_progress(phase: "done")
|
|
557
586
|
end
|
|
558
587
|
end
|
|
559
588
|
return nil
|
|
@@ -606,11 +635,18 @@ module Clacky
|
|
|
606
635
|
# user/assistant alternation for Bedrock Converse API.
|
|
607
636
|
truncated_text = response[:content] || ""
|
|
608
637
|
truncated_text = "..." if truncated_text.strip.empty?
|
|
609
|
-
|
|
638
|
+
truncated_msg = {
|
|
610
639
|
role: "assistant",
|
|
611
640
|
content: truncated_text,
|
|
612
641
|
task_id: @current_task_id
|
|
613
|
-
}
|
|
642
|
+
}
|
|
643
|
+
# Preserve reasoning_content on truncated turns as well.
|
|
644
|
+
# This is the real LLM-emitted reasoning — keeping it here lets
|
|
645
|
+
# MessageHistory#to_api recognize we're in thinking mode and pad any
|
|
646
|
+
# other synthetic assistant messages in the history with an empty
|
|
647
|
+
# reasoning_content automatically (see message_history.rb).
|
|
648
|
+
truncated_msg[:reasoning_content] = response[:reasoning_content] if response[:reasoning_content]
|
|
649
|
+
@history.append(truncated_msg)
|
|
614
650
|
|
|
615
651
|
# Insert system message to guide LLM to retry with smaller steps
|
|
616
652
|
@history.append({
|
|
@@ -644,8 +680,12 @@ module Clacky
|
|
|
644
680
|
end
|
|
645
681
|
# Store token_usage in the message so replay_history can re-emit it
|
|
646
682
|
msg[:token_usage] = response[:token_usage] if response[:token_usage]
|
|
647
|
-
# Preserve reasoning_content
|
|
648
|
-
#
|
|
683
|
+
# Preserve reasoning_content from the real LLM response.
|
|
684
|
+
# This is the authoritative signal used by MessageHistory#to_api to
|
|
685
|
+
# detect thinking-mode providers (DeepSeek V4, Kimi K2 thinking, etc.)
|
|
686
|
+
# and automatically pad any synthetic assistant messages with an empty
|
|
687
|
+
# reasoning_content so every outgoing payload satisfies the provider's
|
|
688
|
+
# "reasoning_content must be passed back" contract.
|
|
649
689
|
msg[:reasoning_content] = response[:reasoning_content] if response[:reasoning_content]
|
|
650
690
|
@history.append(msg)
|
|
651
691
|
|
|
@@ -744,21 +784,31 @@ module Clacky
|
|
|
744
784
|
args[:working_dir] = @working_dir if @working_dir
|
|
745
785
|
|
|
746
786
|
# Show progress immediately for every tool execution so the user
|
|
747
|
-
# always knows the agent is working.
|
|
748
|
-
#
|
|
749
|
-
#
|
|
750
|
-
#
|
|
751
|
-
|
|
787
|
+
# always knows the agent is working. Using +with_progress+ wraps
|
|
788
|
+
# the execution in an +ensure+ block so the spinner/ticker is
|
|
789
|
+
# released even if the tool raises or the user interrupts.
|
|
790
|
+
#
|
|
791
|
+
# +quiet_on_fast_finish: true+ means "if the tool completes in
|
|
792
|
+
# under FAST_FINISH_THRESHOLD_SECONDS, remove the progress line
|
|
793
|
+
# instead of leaving a permanent 'Executing edit… (0s)' log
|
|
794
|
+
# entry". The preceding `[=>] Edit(...)` tool-call line and the
|
|
795
|
+
# following `[<=] Modified 1 occurrence` result line already
|
|
796
|
+
# tell the full story — the middle progress frame is noise for
|
|
797
|
+
# instant tools like edit/write/read/glob/grep. Truly slow
|
|
798
|
+
# tools (terminal running a build, web_fetch) exceed the
|
|
799
|
+
# threshold and their final frame is preserved as usual.
|
|
800
|
+
result = nil
|
|
752
801
|
if @ui
|
|
753
802
|
progress_message = build_tool_progress_message(call[:name], args)
|
|
754
|
-
@ui.
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
803
|
+
@ui.with_progress(
|
|
804
|
+
message: progress_message,
|
|
805
|
+
style: :quiet,
|
|
806
|
+
quiet_on_fast_finish: true
|
|
807
|
+
) do
|
|
808
|
+
result = tool.execute(**args)
|
|
809
|
+
end
|
|
810
|
+
else
|
|
759
811
|
result = tool.execute(**args)
|
|
760
|
-
ensure
|
|
761
|
-
@ui&.show_progress(phase: "done") if progress_shown
|
|
762
812
|
end
|
|
763
813
|
|
|
764
814
|
# Track modified files for Time Machine snapshots
|
|
@@ -990,12 +1040,34 @@ module Clacky
|
|
|
990
1040
|
# Switch to specified model if provided
|
|
991
1041
|
if model
|
|
992
1042
|
if model == "lite"
|
|
993
|
-
# Special keyword: use lite model if available, otherwise fall back to default
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
1043
|
+
# Special keyword: use lite model if available, otherwise fall back to default.
|
|
1044
|
+
#
|
|
1045
|
+
# Lite is now a *virtual* role — we don't require it to exist as a
|
|
1046
|
+
# concrete entry in @models. Instead we derive it from whatever
|
|
1047
|
+
# model the user is currently on (current_model), so switching
|
|
1048
|
+
# primary models automatically re-pairs with the right lite
|
|
1049
|
+
# companion (Claude → Haiku, DeepSeek V4-pro → V4-flash, ...).
|
|
1050
|
+
lite_cfg = subagent_config.lite_model_config_for_current
|
|
1051
|
+
if lite_cfg
|
|
1052
|
+
if lite_cfg["virtual"]
|
|
1053
|
+
# Provider-preset derived: apply the lite fields as a *session
|
|
1054
|
+
# overlay* on the subagent's config — this intentionally avoids
|
|
1055
|
+
# mutating the shared @models array / hashes which would pollute
|
|
1056
|
+
# the parent agent's own current model (e.g. turning the parent's
|
|
1057
|
+
# Opus entry into Haiku for the rest of the session).
|
|
1058
|
+
subagent_config.apply_virtual_model_overlay!(
|
|
1059
|
+
"api_key" => lite_cfg["api_key"],
|
|
1060
|
+
"base_url" => lite_cfg["base_url"],
|
|
1061
|
+
"model" => lite_cfg["model"],
|
|
1062
|
+
"anthropic_format" => lite_cfg["anthropic_format"]
|
|
1063
|
+
)
|
|
1064
|
+
elsif lite_cfg["id"]
|
|
1065
|
+
# Explicit user-configured lite (from CLACKY_LITE_* env): a
|
|
1066
|
+
# real @models entry with a stable id. Switch to it normally.
|
|
1067
|
+
subagent_config.switch_model_by_id(lite_cfg["id"])
|
|
1068
|
+
end
|
|
997
1069
|
end
|
|
998
|
-
# If no lite
|
|
1070
|
+
# If no lite is resolvable, just use current (primary) model.
|
|
999
1071
|
else
|
|
1000
1072
|
# Regular model name lookup — find the first model with a matching
|
|
1001
1073
|
# name and switch by its stable id.
|
|
@@ -1154,12 +1226,29 @@ module Clacky
|
|
|
1154
1226
|
# Resolve image files to vision data_urls.
|
|
1155
1227
|
# Files with data_url: use as-is (already compressed by frontend or adapter).
|
|
1156
1228
|
# Files with path: convert to data_url via FileProcessor.
|
|
1157
|
-
#
|
|
1158
|
-
#
|
|
1229
|
+
#
|
|
1230
|
+
# Downgrade to disk file refs (with a `downgrade_reason` tag) when:
|
|
1231
|
+
# - :provider_no_vision — current model does not support vision input
|
|
1232
|
+
# (e.g. MiniMax, Kimi, DeepSeek, or openclacky's DeepSeek sidecar).
|
|
1233
|
+
# The downgrade is capability-driven and reflects the *current* model;
|
|
1234
|
+
# switching models takes effect on the next run with no cached state.
|
|
1235
|
+
# - :too_large — base64 payload exceeds MAX_IMAGE_BYTES. Downgrading here
|
|
1236
|
+
# keeps a hot context window from blowing up on e.g. a 20MB screenshot.
|
|
1237
|
+
#
|
|
1238
|
+
# Both reasons share the same downgrade path; `file_prompt` will later
|
|
1239
|
+
# emit a `Note:` line on the file entry explaining why the image isn't
|
|
1240
|
+
# inline, so the LLM has colocated context (no system prompt pollution).
|
|
1241
|
+
#
|
|
1242
|
+
# @return [Array<Hash>, Array<Hash>] [vision_images, downgraded_disk_files]
|
|
1159
1243
|
private def resolve_vision_images(image_files)
|
|
1160
1244
|
require "base64"
|
|
1161
1245
|
max_bytes = Utils::FileProcessor::MAX_IMAGE_BYTES
|
|
1162
|
-
|
|
1246
|
+
# Capability check once per run — current_model_supports? is cheap and
|
|
1247
|
+
# delegates to Providers.supports? under the hood, always reflecting
|
|
1248
|
+
# the current model (no stale state on `/model` switch).
|
|
1249
|
+
vision_supported = @config.current_model_supports?(:vision)
|
|
1250
|
+
|
|
1251
|
+
vision_images = [] # Array of { url:, name:, size_bytes:, path: }
|
|
1163
1252
|
downgraded = []
|
|
1164
1253
|
|
|
1165
1254
|
image_files.each do |f|
|
|
@@ -1173,8 +1262,10 @@ module Clacky
|
|
|
1173
1262
|
byte_size = (b64_data.bytesize * 3) / 4
|
|
1174
1263
|
raw = Base64.decode64(b64_data)
|
|
1175
1264
|
file_ref = Utils::FileProcessor.save_image_to_disk(body: raw, mime_type: mime, filename: name)
|
|
1176
|
-
|
|
1177
|
-
|
|
1265
|
+
reason = downgrade_reason_for(vision_supported, byte_size, max_bytes)
|
|
1266
|
+
if reason
|
|
1267
|
+
downgraded << { name: name, path: file_ref.original_path, type: "image",
|
|
1268
|
+
mime_type: mime, size_bytes: byte_size, downgrade_reason: reason }
|
|
1178
1269
|
else
|
|
1179
1270
|
vision_images << { url: data_url, name: name, size_bytes: byte_size, path: file_ref.original_path }
|
|
1180
1271
|
end
|
|
@@ -1183,8 +1274,10 @@ module Clacky
|
|
|
1183
1274
|
data_url_from_path = Utils::FileProcessor.image_path_to_data_url(path)
|
|
1184
1275
|
b64_data = data_url_from_path.split(",", 2).last.to_s
|
|
1185
1276
|
byte_size = (b64_data.bytesize * 3) / 4
|
|
1186
|
-
|
|
1187
|
-
|
|
1277
|
+
reason = downgrade_reason_for(vision_supported, byte_size, max_bytes)
|
|
1278
|
+
if reason
|
|
1279
|
+
downgraded << { name: name, path: path, type: "image",
|
|
1280
|
+
mime_type: mime, size_bytes: byte_size, downgrade_reason: reason }
|
|
1188
1281
|
else
|
|
1189
1282
|
vision_images << { url: data_url_from_path, name: name, size_bytes: byte_size, path: path }
|
|
1190
1283
|
end
|
|
@@ -1197,6 +1290,30 @@ module Clacky
|
|
|
1197
1290
|
[vision_images, downgraded]
|
|
1198
1291
|
end
|
|
1199
1292
|
|
|
1293
|
+
# Decide whether an image must be downgraded to a disk ref, and if so why.
|
|
1294
|
+
# Precedence: provider capability is checked first — a text-only model
|
|
1295
|
+
# can't use the image at any size, so there's no point re-checking size.
|
|
1296
|
+
# @return [Symbol, nil] :provider_no_vision | :too_large | nil (keep inline)
|
|
1297
|
+
private def downgrade_reason_for(vision_supported, byte_size, max_bytes)
|
|
1298
|
+
return :provider_no_vision unless vision_supported
|
|
1299
|
+
return :too_large if byte_size > max_bytes
|
|
1300
|
+
nil
|
|
1301
|
+
end
|
|
1302
|
+
|
|
1303
|
+
# Human-readable note for a downgrade reason, embedded next to the file
|
|
1304
|
+
# entry in the file_prompt. Kept intentionally terse and factual; the LLM
|
|
1305
|
+
# will see this alongside the file's name/type/path so it can tell the
|
|
1306
|
+
# user honestly why it can't see the image.
|
|
1307
|
+
# @return [String, nil] note text, or nil for no note
|
|
1308
|
+
private def downgrade_note_for(reason)
|
|
1309
|
+
case reason&.to_sym
|
|
1310
|
+
when :provider_no_vision
|
|
1311
|
+
"The current model does not support vision input. Image content is not visible to the model; suggest switching to a vision-capable model if the user needs image analysis."
|
|
1312
|
+
when :too_large
|
|
1313
|
+
"Image was too large for inline delivery and has been saved to disk. Read it with a vision-capable tool/model if needed."
|
|
1314
|
+
end
|
|
1315
|
+
end
|
|
1316
|
+
|
|
1200
1317
|
# Build user message content for LLM.
|
|
1201
1318
|
# Returns plain String when no vision images; Array of content parts otherwise.
|
|
1202
1319
|
# Build user message content for LLM.
|
|
@@ -1254,7 +1371,19 @@ module Clacky
|
|
|
1254
1371
|
# Core method to inject session context (date, model, OS, paths).
|
|
1255
1372
|
# Called by inject_session_context_if_needed (with date check)
|
|
1256
1373
|
# and by switch_model (without date check, to force update).
|
|
1374
|
+
#
|
|
1375
|
+
# IMPORTANT: Skip injection when the system prompt hasn't been built yet.
|
|
1376
|
+
# Otherwise, appending a user message to an empty history makes
|
|
1377
|
+
# @history.empty? false, which causes run() to skip building the
|
|
1378
|
+
# system prompt entirely (see run()'s "first run" guard).
|
|
1379
|
+
# The injection will happen naturally in run() via
|
|
1380
|
+
# inject_session_context_if_needed after the system prompt is in place.
|
|
1257
1381
|
private def inject_session_context
|
|
1382
|
+
# Don't inject context before system prompt exists — defer to
|
|
1383
|
+
# inject_session_context_if_needed which runs inside run()
|
|
1384
|
+
# after the system prompt has been built.
|
|
1385
|
+
return unless @history.has_system_prompt?
|
|
1386
|
+
|
|
1258
1387
|
today = Time.now.strftime("%Y-%m-%d")
|
|
1259
1388
|
os = Clacky::Utils::EnvironmentDetector.os_type
|
|
1260
1389
|
desktop = Clacky::Utils::EnvironmentDetector.desktop_path
|