openclacky 1.4.0 → 1.4.1
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 +29 -0
- data/lib/clacky/agent/session_serializer.rb +11 -4
- data/lib/clacky/agent.rb +4 -5
- data/lib/clacky/brand_config.rb +53 -3
- data/lib/clacky/default_extensions/ext-studio/api/handler.rb +144 -1
- data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +412 -173
- data/lib/clacky/extension/packager.rb +10 -1
- data/lib/clacky/platform_http_client.rb +9 -7
- data/lib/clacky/providers.rb +4 -4
- data/lib/clacky/server/http_server.rb +19 -18
- data/lib/clacky/tools/grep.rb +6 -1
- data/lib/clacky/utils/environment_detector.rb +16 -9
- data/lib/clacky/utils/file_processor.rb +21 -24
- data/lib/clacky/utils/model_pricing.rb +49 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +121 -17
- data/lib/clacky/web/components/onboard.js +14 -4
- data/lib/clacky/web/features/backup/view.js +4 -3
- data/lib/clacky/web/features/extensions/store.js +2 -2
- data/lib/clacky/web/features/extensions/view.js +14 -19
- data/lib/clacky/web/features/mcp/view.js +5 -2
- data/lib/clacky/web/features/workspace/store.js +11 -0
- data/lib/clacky/web/features/workspace/view.js +10 -4
- data/lib/clacky/web/i18n.js +6 -18
- data/lib/clacky/web/index.html +23 -2
- data/lib/clacky/web/sessions.js +33 -5
- data/scripts/uninstall.sh +1 -1
- metadata +1 -1
|
@@ -195,7 +195,9 @@ module Clacky
|
|
|
195
195
|
dirs = children.select { |c| File.directory?(File.join(root, c)) }
|
|
196
196
|
|
|
197
197
|
if File.file?(File.join(root, MANIFEST))
|
|
198
|
-
|
|
198
|
+
id = manifest_id(File.join(root, MANIFEST))
|
|
199
|
+
raise Error, "archive has no top-level container and #{MANIFEST} declares no id" if id.empty?
|
|
200
|
+
return [id, root]
|
|
199
201
|
end
|
|
200
202
|
|
|
201
203
|
if dirs.size == 1 && File.file?(File.join(root, dirs.first, MANIFEST))
|
|
@@ -205,6 +207,13 @@ module Clacky
|
|
|
205
207
|
raise Error, "archive does not contain a single container with #{MANIFEST}"
|
|
206
208
|
end
|
|
207
209
|
|
|
210
|
+
private def manifest_id(manifest_path)
|
|
211
|
+
data = YAMLCompat.load_file(manifest_path)
|
|
212
|
+
data.is_a?(Hash) ? data["id"].to_s.strip : ""
|
|
213
|
+
rescue StandardError
|
|
214
|
+
""
|
|
215
|
+
end
|
|
216
|
+
|
|
208
217
|
private def verify_installed(ext_id, installed_dir)
|
|
209
218
|
layers =
|
|
210
219
|
if installed_dir == Clacky::ExtensionLoader::INSTALLED_DIR
|
|
@@ -24,17 +24,19 @@ module Clacky
|
|
|
24
24
|
# # result => { success: true, data: {...} }
|
|
25
25
|
# # or { success: false, error: "...", data: {} }
|
|
26
26
|
class PlatformHttpClient
|
|
27
|
-
# Primary
|
|
28
|
-
PRIMARY_HOST
|
|
27
|
+
# Primary endpoint
|
|
28
|
+
PRIMARY_HOST = "https://www.openclacky.com"
|
|
29
|
+
# Secondary CDN-accelerated endpoint (China mainland)
|
|
30
|
+
SECONDARY_HOST = "https://api.1024code.com"
|
|
29
31
|
# Direct fallback — bypasses EdgeOne, used when the primary times out
|
|
30
|
-
FALLBACK_HOST
|
|
32
|
+
FALLBACK_HOST = "https://openclacky.up.railway.app"
|
|
31
33
|
|
|
32
34
|
# Number of attempts per domain (1 = no retry within the same domain)
|
|
33
|
-
ATTEMPTS_PER_HOST =
|
|
35
|
+
ATTEMPTS_PER_HOST = 1
|
|
34
36
|
# Initial back-off between retries within the same domain (seconds)
|
|
35
37
|
INITIAL_BACKOFF = 0.5
|
|
36
38
|
# Connection / read timeouts (seconds) for API calls
|
|
37
|
-
OPEN_TIMEOUT =
|
|
39
|
+
OPEN_TIMEOUT = 5
|
|
38
40
|
READ_TIMEOUT = 15
|
|
39
41
|
# Read timeout for streaming large file downloads (seconds)
|
|
40
42
|
DOWNLOAD_READ_TIMEOUT = 120
|
|
@@ -53,12 +55,12 @@ module Clacky
|
|
|
53
55
|
|
|
54
56
|
# Auto-detects the target host(s):
|
|
55
57
|
# - When CLACKY_LICENSE_SERVER is set → single host (dev override, no failover)
|
|
56
|
-
# - Otherwise → [PRIMARY_HOST, FALLBACK_HOST]
|
|
58
|
+
# - Otherwise → [PRIMARY_HOST, SECONDARY_HOST, FALLBACK_HOST]
|
|
57
59
|
def initialize
|
|
58
60
|
if (override = ENV["CLACKY_LICENSE_SERVER"]) && !override.empty?
|
|
59
61
|
@hosts = [override]
|
|
60
62
|
else
|
|
61
|
-
@hosts = [PRIMARY_HOST, FALLBACK_HOST]
|
|
63
|
+
@hosts = [PRIMARY_HOST, SECONDARY_HOST, FALLBACK_HOST]
|
|
62
64
|
end
|
|
63
65
|
end
|
|
64
66
|
|
data/lib/clacky/providers.rb
CHANGED
|
@@ -260,8 +260,8 @@ module Clacky
|
|
|
260
260
|
"name" => "Kimi (Moonshot)",
|
|
261
261
|
"base_url" => "https://api.moonshot.cn/v1",
|
|
262
262
|
"api" => "openai-completions",
|
|
263
|
-
"default_model" => "kimi-
|
|
264
|
-
"models" => ["kimi-k2.6", "kimi-k2.5"],
|
|
263
|
+
"default_model" => "kimi-k3",
|
|
264
|
+
"models" => ["kimi-k3", "kimi-k2.7-code", "kimi-k2.7-code-highspeed", "kimi-k2.6", "kimi-k2.5"],
|
|
265
265
|
# Moonshot operates two regional endpoints with identical APIs & model
|
|
266
266
|
# lineup — mainland China (.cn) and international (.ai). These are the
|
|
267
267
|
# pay-as-you-go Open Platform endpoints; the subscription-billed
|
|
@@ -277,9 +277,9 @@ module Clacky
|
|
|
277
277
|
{ "label" => "Mainland China", "label_key" => "settings.models.baseurl.variant.mainland_cn", "base_url" => "https://api.moonshot.cn/v1", "region" => "cn" }.freeze,
|
|
278
278
|
{ "label" => "International", "label_key" => "settings.models.baseurl.variant.international", "base_url" => "https://api.moonshot.ai/v1", "region" => "intl" }.freeze
|
|
279
279
|
].freeze,
|
|
280
|
-
# k2.5 / k2.6 are multimodal; legacy k2 text-only models need model_capabilities override if added.
|
|
280
|
+
# k3 / k2.7-code / k2.5 / k2.6 are multimodal; legacy k2 text-only models need model_capabilities override if added.
|
|
281
281
|
"capabilities" => { "vision" => true }.freeze,
|
|
282
|
-
"default_ocr_model" => "kimi-
|
|
282
|
+
"default_ocr_model" => "kimi-k3",
|
|
283
283
|
"website_url" => "https://platform.moonshot.cn/console/api-keys"
|
|
284
284
|
}.freeze,
|
|
285
285
|
|
|
@@ -36,9 +36,10 @@ module Clacky
|
|
|
36
36
|
@events = events
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
-
def show_user_message(content, created_at: nil, files: [])
|
|
39
|
+
def show_user_message(content, created_at: nil, files: [], editable: true)
|
|
40
40
|
ev = { type: "history_user_message", session_id: @session_id, content: content }
|
|
41
41
|
ev[:created_at] = created_at if created_at
|
|
42
|
+
ev[:editable] = false unless editable
|
|
42
43
|
rendered = Array(files).filter_map do |f|
|
|
43
44
|
url = f[:data_url] || f["data_url"]
|
|
44
45
|
name = f[:name] || f["name"]
|
|
@@ -964,11 +965,13 @@ module Clacky
|
|
|
964
965
|
# Phase "key_setup" → no API key configured yet
|
|
965
966
|
# Phase "soul_setup" → key configured, but ~/.clacky/agents/SOUL.md missing
|
|
966
967
|
# needs_onboard: false → fully set up
|
|
968
|
+
# branded: true → running under a brand; hide the OpenClacky AI Keys block
|
|
967
969
|
def api_onboard_status(res)
|
|
970
|
+
branded = Clacky::BrandConfig.load.branded?
|
|
968
971
|
if !@agent_config.models_configured?
|
|
969
|
-
json_response(res, 200, { needs_onboard: true, phase: "key_setup" })
|
|
972
|
+
json_response(res, 200, { needs_onboard: true, phase: "key_setup", branded: branded })
|
|
970
973
|
else
|
|
971
|
-
json_response(res, 200, { needs_onboard: false })
|
|
974
|
+
json_response(res, 200, { needs_onboard: false, branded: branded })
|
|
972
975
|
end
|
|
973
976
|
end
|
|
974
977
|
|
|
@@ -2408,6 +2411,8 @@ module Clacky
|
|
|
2408
2411
|
{
|
|
2409
2412
|
"id" => ext_id,
|
|
2410
2413
|
"name" => market ? (market["name"] || ext_id) : ext_id,
|
|
2414
|
+
"display_name" => market&.dig("display_name"),
|
|
2415
|
+
"display_name_zh" => market&.dig("display_name_zh"),
|
|
2411
2416
|
"name_zh" => market&.dig("name_zh"),
|
|
2412
2417
|
"name_en" => market&.dig("name_en"),
|
|
2413
2418
|
"slug" => ext_id,
|
|
@@ -3691,13 +3696,11 @@ module Clacky
|
|
|
3691
3696
|
|
|
3692
3697
|
return json_response(res, 400, { error: "path is required" }) unless path && !path.empty?
|
|
3693
3698
|
|
|
3694
|
-
#
|
|
3695
|
-
#
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
# On WSL the file may be specified as a Windows path (e.g. "C:/Users/…").
|
|
3699
|
-
# Convert it to the Linux-side path so File.exist? works.
|
|
3699
|
+
# Path arrives already percent-decoded by the click handler; normalize
|
|
3700
|
+
# Windows drive letters (WSL) BEFORE expand_path or the drive is treated
|
|
3701
|
+
# as relative and corrupted. No-op on macOS/Linux.
|
|
3700
3702
|
linux_path = Utils::EnvironmentDetector.win_to_linux_path(path)
|
|
3703
|
+
linux_path = File.expand_path(linux_path)
|
|
3701
3704
|
|
|
3702
3705
|
return json_response(res, 404, { error: "file not found" }) unless File.exist?(linux_path)
|
|
3703
3706
|
|
|
@@ -3711,8 +3714,11 @@ module Clacky
|
|
|
3711
3714
|
json_response(res, 200, { ok: true })
|
|
3712
3715
|
when "download"
|
|
3713
3716
|
serve_file_download(res, linux_path)
|
|
3717
|
+
when "display-path"
|
|
3718
|
+
display = Utils::EnvironmentDetector.linux_to_win_path(linux_path)
|
|
3719
|
+
json_response(res, 200, { ok: true, path: display })
|
|
3714
3720
|
else
|
|
3715
|
-
json_response(res, 400, { error: "invalid action. Must be 'open', 'reveal' or '
|
|
3721
|
+
json_response(res, 400, { error: "invalid action. Must be 'open', 'reveal', 'download' or 'display-path'" })
|
|
3716
3722
|
end
|
|
3717
3723
|
rescue => e
|
|
3718
3724
|
json_response(res, 500, { ok: false, error: e.message })
|
|
@@ -3742,14 +3748,9 @@ module Clacky
|
|
|
3742
3748
|
raw_path = URI.decode_www_form(req.query_string.to_s).to_h["path"].to_s
|
|
3743
3749
|
return json_response(res, 400, { error: "path is required" }) if raw_path.empty?
|
|
3744
3750
|
|
|
3745
|
-
# Strip file
|
|
3746
|
-
|
|
3747
|
-
path =
|
|
3748
|
-
path = File.expand_path(path)
|
|
3749
|
-
|
|
3750
|
-
# On WSL the file may be specified as a Windows path (e.g. "C:/Users/…").
|
|
3751
|
-
# Convert it to the Linux-side path so File.exist? works.
|
|
3752
|
-
path = Utils::EnvironmentDetector.win_to_linux_path(path)
|
|
3751
|
+
# Strip file://, decode, WSL drive-letter normalize, then expand — in
|
|
3752
|
+
# this order (normalize must precede expand_path). No-op on macOS/Linux.
|
|
3753
|
+
path = Utils::EnvironmentDetector.resolve_local_path(raw_path)
|
|
3753
3754
|
|
|
3754
3755
|
# Security: only serve media files (images + videos)
|
|
3755
3756
|
ext = File.extname(path).downcase
|
data/lib/clacky/tools/grep.rb
CHANGED
|
@@ -121,12 +121,17 @@ module Clacky
|
|
|
121
121
|
files = if File.file?(expanded_path)
|
|
122
122
|
[expanded_path]
|
|
123
123
|
else
|
|
124
|
+
# Auto-expand bare extension patterns (e.g. "*.ts") to recursive
|
|
125
|
+
# (e.g. "**/*.ts"), matching ripgrep's default behaviour.
|
|
126
|
+
# Patterns already containing "/" are left as-is — the user is
|
|
127
|
+
# intentionally scoping to a specific directory.
|
|
128
|
+
effective_pattern = file_pattern.include?("/") ? file_pattern : "**/#{file_pattern}"
|
|
124
129
|
fnmatch_flags = File::FNM_PATHNAME | File::FNM_DOTMATCH
|
|
125
130
|
collected = []
|
|
126
131
|
walk_status = {}
|
|
127
132
|
Clacky::Utils::FileIgnoreHelper.walk_files(expanded_path, skipped: skipped, status: walk_status) do |f|
|
|
128
133
|
relative = f[(expanded_path.length + 1)..]
|
|
129
|
-
collected << f if File.fnmatch(
|
|
134
|
+
collected << f if File.fnmatch(effective_pattern, relative, fnmatch_flags)
|
|
130
135
|
end
|
|
131
136
|
if walk_status[:truncated]
|
|
132
137
|
truncation_reason ||= "walk #{walk_status[:truncation_reason]}"
|
|
@@ -52,17 +52,24 @@ module Clacky
|
|
|
52
52
|
end
|
|
53
53
|
end
|
|
54
54
|
|
|
55
|
-
# Convert a Windows-style path
|
|
56
|
-
#
|
|
57
|
-
# Returns the original path unchanged on non-WSL or if already a Linux path.
|
|
58
|
-
# @param path [String]
|
|
59
|
-
# @return [String]
|
|
55
|
+
# Convert a Windows-style path (incl. the "/C:/" three-slash-URL form) to
|
|
56
|
+
# a WSL /mnt path. No-op on non-WSL or non-drive-letter paths.
|
|
60
57
|
def self.win_to_linux_path(path)
|
|
61
|
-
return path unless os_type == :wsl
|
|
58
|
+
return path unless os_type == :wsl
|
|
59
|
+
|
|
60
|
+
m = path.match(%r{\A/?([A-Za-z]):[/\\](.*)}m)
|
|
61
|
+
return path unless m
|
|
62
|
+
|
|
63
|
+
"/mnt/#{m[1].downcase}/#{m[2].gsub("\\", "/")}"
|
|
64
|
+
end
|
|
62
65
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
+
# Resolve a "file://" URL or bare path into a real local path:
|
|
67
|
+
# strip "file://", percent-decode, WSL drive-letter normalize, then expand.
|
|
68
|
+
def self.resolve_local_path(href)
|
|
69
|
+
path = href.to_s.sub(%r{\Afile://}, "")
|
|
70
|
+
path = CGI.unescape(path)
|
|
71
|
+
path = win_to_linux_path(path)
|
|
72
|
+
File.expand_path(path)
|
|
66
73
|
end
|
|
67
74
|
|
|
68
75
|
# Convert a Linux-side path to a Windows-style path via wslpath.
|
|
@@ -6,6 +6,7 @@ require "securerandom"
|
|
|
6
6
|
require "stringio"
|
|
7
7
|
|
|
8
8
|
require_relative "parser_manager"
|
|
9
|
+
require_relative "environment_detector"
|
|
9
10
|
require "zip"
|
|
10
11
|
|
|
11
12
|
module Clacky
|
|
@@ -595,67 +596,63 @@ module Clacky
|
|
|
595
596
|
return content if content.nil? || content.empty?
|
|
596
597
|
|
|
597
598
|
# Rewrite markdown image syntax  → proxy URL
|
|
598
|
-
content = content.gsub(/!\[([^\]]*)\]\(((?:file:\/\/)
|
|
599
|
+
content = content.gsub(/!\[([^\]]*)\]\(((?:file:\/\/)?(?:~\/|\/)[^)]+)\)/) do |_match|
|
|
599
600
|
alt = Regexp.last_match(1)
|
|
600
601
|
href = Regexp.last_match(2)
|
|
601
602
|
|
|
602
|
-
|
|
603
|
-
path = CGI.unescape(path)
|
|
603
|
+
real = Clacky::Utils::EnvironmentDetector.resolve_local_path(href)
|
|
604
604
|
|
|
605
|
-
ext = File.extname(
|
|
606
|
-
if LOCAL_MEDIA_EXTENSIONS.include?(ext) && File.exist?(
|
|
607
|
-
".downcase
|
|
606
|
+
if LOCAL_MEDIA_EXTENSIONS.include?(ext) && File.exist?(real)
|
|
607
|
+
"})"
|
|
608
608
|
else
|
|
609
609
|
_match
|
|
610
610
|
end
|
|
611
611
|
end
|
|
612
612
|
|
|
613
613
|
# Rewrite <video src="file:///path/vid.mp4" ...> → proxy URL
|
|
614
|
-
content = content.gsub(/<video\b([^>]*)\bsrc="((?:file:\/\/)
|
|
614
|
+
content = content.gsub(/<video\b([^>]*)\bsrc="((?:file:\/\/)?(?:~\/|\/)[^"]+)"([^>]*)>/) do |_match|
|
|
615
615
|
pre = Regexp.last_match(1) || ""
|
|
616
616
|
href = Regexp.last_match(2)
|
|
617
617
|
post = Regexp.last_match(3) || ""
|
|
618
618
|
|
|
619
|
-
|
|
620
|
-
path = CGI.unescape(path)
|
|
619
|
+
real = Clacky::Utils::EnvironmentDetector.resolve_local_path(href)
|
|
621
620
|
|
|
622
|
-
ext = File.extname(
|
|
623
|
-
if LOCAL_VIDEO_EXTENSIONS.include?(ext) && File.exist?(
|
|
624
|
-
"<video#{pre} src=\"#{local_image_proxy_url(href,
|
|
621
|
+
ext = File.extname(real).downcase
|
|
622
|
+
if LOCAL_VIDEO_EXTENSIONS.include?(ext) && File.exist?(real)
|
|
623
|
+
"<video#{pre} src=\"#{local_image_proxy_url(href, real)}\"#{post}>"
|
|
625
624
|
else
|
|
626
625
|
_match
|
|
627
626
|
end
|
|
628
627
|
end
|
|
629
628
|
|
|
630
629
|
# Rewrite <audio src="file:///path/audio.wav" ...> → proxy URL
|
|
631
|
-
content = content.gsub(/<audio\b([^>]*)\bsrc="((?:file:\/\/)
|
|
630
|
+
content = content.gsub(/<audio\b([^>]*)\bsrc="((?:file:\/\/)?(?:~\/|\/)[^"]+)"([^>]*)>/) do |_match|
|
|
632
631
|
pre = Regexp.last_match(1) || ""
|
|
633
632
|
href = Regexp.last_match(2)
|
|
634
633
|
post = Regexp.last_match(3) || ""
|
|
635
634
|
|
|
636
|
-
|
|
637
|
-
path = CGI.unescape(path)
|
|
635
|
+
real = Clacky::Utils::EnvironmentDetector.resolve_local_path(href)
|
|
638
636
|
|
|
639
|
-
ext = File.extname(
|
|
640
|
-
if LOCAL_AUDIO_EXTENSIONS.include?(ext) && File.exist?(
|
|
641
|
-
"<audio#{pre} src=\"#{local_image_proxy_url(href,
|
|
637
|
+
ext = File.extname(real).downcase
|
|
638
|
+
if LOCAL_AUDIO_EXTENSIONS.include?(ext) && File.exist?(real)
|
|
639
|
+
"<audio#{pre} src=\"#{local_image_proxy_url(href, real)}\"#{post}>"
|
|
642
640
|
else
|
|
643
641
|
_match
|
|
644
642
|
end
|
|
645
643
|
end
|
|
646
644
|
|
|
647
645
|
# Rewrite video/audio markdown links [text](file:///path) → proxy URL
|
|
648
|
-
content = content.gsub(/(?<!!)\[([^\]]*)\]\(((?:file:\/\/)
|
|
646
|
+
content = content.gsub(/(?<!!)\[([^\]]*)\]\(((?:file:\/\/)?(?:~\/|\/)[^)]+)\)/) do |_match|
|
|
649
647
|
text = Regexp.last_match(1)
|
|
650
648
|
href = Regexp.last_match(2)
|
|
651
649
|
|
|
652
|
-
|
|
653
|
-
path = CGI.unescape(path)
|
|
650
|
+
real = Clacky::Utils::EnvironmentDetector.resolve_local_path(href)
|
|
654
651
|
|
|
655
|
-
ext = File.extname(
|
|
652
|
+
ext = File.extname(real).downcase
|
|
656
653
|
if LOCAL_VIDEO_EXTENSIONS.include?(ext) || LOCAL_AUDIO_EXTENSIONS.include?(ext)
|
|
657
|
-
if File.exist?(
|
|
658
|
-
"[#{text}](#{local_image_proxy_url(href,
|
|
654
|
+
if File.exist?(real)
|
|
655
|
+
"[#{text}](#{local_image_proxy_url(href, real)})"
|
|
659
656
|
else
|
|
660
657
|
_match
|
|
661
658
|
end
|
|
@@ -237,6 +237,55 @@ module Clacky
|
|
|
237
237
|
}
|
|
238
238
|
},
|
|
239
239
|
|
|
240
|
+
# Kimi K3 flagship model (1M context, native vision, tool calling).
|
|
241
|
+
# Source: https://platform.moonshot.ai (USD / 1M tokens)
|
|
242
|
+
"kimi-k3" => {
|
|
243
|
+
input: {
|
|
244
|
+
default: 3.00, # $3.00/MTok cache miss
|
|
245
|
+
over_200k: 3.00
|
|
246
|
+
},
|
|
247
|
+
output: {
|
|
248
|
+
default: 15.00, # $15.00/MTok
|
|
249
|
+
over_200k: 15.00
|
|
250
|
+
},
|
|
251
|
+
cache: {
|
|
252
|
+
write: 3.00, # no separate write charge; bill at miss rate
|
|
253
|
+
read: 0.30 # $0.30/MTok cache hit
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
|
|
257
|
+
# Kimi K2.7 Code (256K context, multimodal coding model).
|
|
258
|
+
# Source: https://platform.moonshot.ai (USD / 1M tokens)
|
|
259
|
+
"kimi-k2.7-code" => {
|
|
260
|
+
input: {
|
|
261
|
+
default: 0.95, # $0.95/MTok cache miss
|
|
262
|
+
over_200k: 0.95
|
|
263
|
+
},
|
|
264
|
+
output: {
|
|
265
|
+
default: 4.00, # $4.00/MTok
|
|
266
|
+
over_200k: 4.00
|
|
267
|
+
},
|
|
268
|
+
cache: {
|
|
269
|
+
write: 0.95, # no separate write charge; bill at miss rate
|
|
270
|
+
read: 0.19 # $0.19/MTok cache hit
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
|
|
274
|
+
"kimi-k2.7-code-highspeed" => {
|
|
275
|
+
input: {
|
|
276
|
+
default: 1.90, # $1.90/MTok cache miss
|
|
277
|
+
over_200k: 1.90
|
|
278
|
+
},
|
|
279
|
+
output: {
|
|
280
|
+
default: 8.00, # $8.00/MTok
|
|
281
|
+
over_200k: 8.00
|
|
282
|
+
},
|
|
283
|
+
cache: {
|
|
284
|
+
write: 1.90, # no separate write charge; bill at miss rate
|
|
285
|
+
read: 0.38 # $0.38/MTok cache hit
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
|
|
240
289
|
# Google Gemini 3 series (via Vertex AI). Tiered at 200K input tokens
|
|
241
290
|
# for Pro; Flash has flat pricing.
|
|
242
291
|
"gemini-3.1-pro" => {
|
data/lib/clacky/version.rb
CHANGED
data/lib/clacky/web/app.css
CHANGED
|
@@ -662,11 +662,15 @@ body {
|
|
|
662
662
|
}
|
|
663
663
|
|
|
664
664
|
/* ── New Session button ──────────────────────────────────────────────────── */
|
|
665
|
+
.btn-split-wrap {
|
|
666
|
+
display: flex;
|
|
667
|
+
align-items: stretch;
|
|
668
|
+
}
|
|
665
669
|
.btn-split-main {
|
|
666
670
|
height: 1.5rem;
|
|
667
|
-
padding: 0 0.
|
|
671
|
+
padding: 0 0.6rem;
|
|
668
672
|
border: none;
|
|
669
|
-
border-radius: 6px;
|
|
673
|
+
border-radius: 6px 0 0 6px;
|
|
670
674
|
background: var(--color-accent-primary);
|
|
671
675
|
color: #fff;
|
|
672
676
|
font-size: 0.75rem;
|
|
@@ -674,12 +678,85 @@ body {
|
|
|
674
678
|
white-space: nowrap;
|
|
675
679
|
line-height: 1;
|
|
676
680
|
cursor: pointer;
|
|
681
|
+
display: flex;
|
|
682
|
+
align-items: center;
|
|
683
|
+
gap: 0.3rem;
|
|
677
684
|
transition: background 0.15s, box-shadow 0.15s;
|
|
678
685
|
}
|
|
679
686
|
.btn-split-main:hover {
|
|
680
687
|
background: var(--color-button-primary-hover);
|
|
681
688
|
box-shadow: 0 2px 6px rgba(0,0,0,0.15);
|
|
682
689
|
}
|
|
690
|
+
.btn-split-arrow {
|
|
691
|
+
display: flex;
|
|
692
|
+
align-items: center;
|
|
693
|
+
justify-content: center;
|
|
694
|
+
height: 1.5rem;
|
|
695
|
+
width: 1.375rem;
|
|
696
|
+
padding: 0;
|
|
697
|
+
border: none;
|
|
698
|
+
border-left: 1px solid rgba(255,255,255,0.2);
|
|
699
|
+
border-radius: 0 6px 6px 0;
|
|
700
|
+
background: var(--color-accent-primary);
|
|
701
|
+
color: #fff;
|
|
702
|
+
cursor: pointer;
|
|
703
|
+
transition: background 0.15s, box-shadow 0.15s;
|
|
704
|
+
flex-shrink: 0;
|
|
705
|
+
}
|
|
706
|
+
.btn-split-arrow:hover {
|
|
707
|
+
background: var(--color-button-primary-hover);
|
|
708
|
+
box-shadow: 0 2px 6px rgba(0,0,0,0.15);
|
|
709
|
+
}
|
|
710
|
+
.btn-split-dropdown {
|
|
711
|
+
position: relative;
|
|
712
|
+
display: flex;
|
|
713
|
+
}
|
|
714
|
+
.btn-split-menu {
|
|
715
|
+
display: none;
|
|
716
|
+
position: absolute;
|
|
717
|
+
top: calc(100% + 4px);
|
|
718
|
+
right: 0;
|
|
719
|
+
min-width: 9rem;
|
|
720
|
+
background: var(--color-bg-elevated, var(--color-bg-secondary));
|
|
721
|
+
border: 1px solid var(--color-border-secondary);
|
|
722
|
+
border-radius: 8px;
|
|
723
|
+
box-shadow: 0 4px 16px rgba(0,0,0,0.12);
|
|
724
|
+
padding: 0.25rem;
|
|
725
|
+
z-index: 200;
|
|
726
|
+
white-space: nowrap;
|
|
727
|
+
}
|
|
728
|
+
/* Bridge the 4px gap so hover doesn't break when moving to the menu */
|
|
729
|
+
.btn-split-menu::before {
|
|
730
|
+
content: "";
|
|
731
|
+
position: absolute;
|
|
732
|
+
top: -8px;
|
|
733
|
+
left: 0;
|
|
734
|
+
right: 0;
|
|
735
|
+
height: 8px;
|
|
736
|
+
}
|
|
737
|
+
.btn-split-dropdown:hover .btn-split-menu {
|
|
738
|
+
display: block;
|
|
739
|
+
}
|
|
740
|
+
.btn-split-menu-item {
|
|
741
|
+
display: flex;
|
|
742
|
+
align-items: center;
|
|
743
|
+
gap: 7px;
|
|
744
|
+
width: 100%;
|
|
745
|
+
padding: 0.375rem 0.75rem;
|
|
746
|
+
border: none;
|
|
747
|
+
border-radius: 5px;
|
|
748
|
+
background: transparent;
|
|
749
|
+
color: var(--color-text-secondary);
|
|
750
|
+
font-size: 0.8125rem;
|
|
751
|
+
font-family: var(--font-sans);
|
|
752
|
+
text-align: left;
|
|
753
|
+
cursor: pointer;
|
|
754
|
+
transition: background 0.12s, color 0.12s;
|
|
755
|
+
}
|
|
756
|
+
.btn-split-menu-item:hover {
|
|
757
|
+
background: var(--color-bg-tertiary);
|
|
758
|
+
color: var(--color-text-primary);
|
|
759
|
+
}
|
|
683
760
|
|
|
684
761
|
/* ── Session source badge ────────────────────────────────────────────────── */
|
|
685
762
|
/* Design: compact neutral chips — the visual weight is carried by the
|
|
@@ -7199,7 +7276,7 @@ body {
|
|
|
7199
7276
|
.extension-card:hover { border-color: var(--color-text-muted); }
|
|
7200
7277
|
.extension-card-main {
|
|
7201
7278
|
display: flex;
|
|
7202
|
-
align-items:
|
|
7279
|
+
align-items: center;
|
|
7203
7280
|
gap: 0.75rem;
|
|
7204
7281
|
}
|
|
7205
7282
|
.extension-emoji {
|
|
@@ -7348,6 +7425,38 @@ body {
|
|
|
7348
7425
|
letter-spacing: 0.04em;
|
|
7349
7426
|
margin: 0 0 0.75rem;
|
|
7350
7427
|
}
|
|
7428
|
+
/* Readme rendered Markdown */
|
|
7429
|
+
.extension-readme-body {
|
|
7430
|
+
font-size: 0.875rem;
|
|
7431
|
+
line-height: 1.7;
|
|
7432
|
+
color: var(--color-text-primary);
|
|
7433
|
+
overflow-wrap: break-word;
|
|
7434
|
+
}
|
|
7435
|
+
.extension-readme-body h1,
|
|
7436
|
+
.extension-readme-body h2,
|
|
7437
|
+
.extension-readme-body h3 { margin: 1em 0 0.4em; font-weight: 600; }
|
|
7438
|
+
.extension-readme-body h1 { font-size: 1.2em; }
|
|
7439
|
+
.extension-readme-body h2 { font-size: 1.05em; }
|
|
7440
|
+
.extension-readme-body h3 { font-size: 0.95em; }
|
|
7441
|
+
.extension-readme-body p { margin: 0 0 0.75em; }
|
|
7442
|
+
.extension-readme-body ul,
|
|
7443
|
+
.extension-readme-body ol { padding-left: 1.4em; margin: 0 0 0.75em; }
|
|
7444
|
+
.extension-readme-body pre {
|
|
7445
|
+
background: var(--color-bg-secondary);
|
|
7446
|
+
border-radius: 6px;
|
|
7447
|
+
padding: 0.75em 1em;
|
|
7448
|
+
overflow-x: auto;
|
|
7449
|
+
font-size: 0.8em;
|
|
7450
|
+
}
|
|
7451
|
+
.extension-readme-body code {
|
|
7452
|
+
background: var(--color-bg-secondary);
|
|
7453
|
+
padding: 0.1em 0.3em;
|
|
7454
|
+
border-radius: 3px;
|
|
7455
|
+
font-size: 0.85em;
|
|
7456
|
+
}
|
|
7457
|
+
.extension-readme-body pre code { background: none; padding: 0; }
|
|
7458
|
+
.extension-readme-body a { color: var(--color-accent); }
|
|
7459
|
+
.extension-readme-body img { max-width: 100%; height: auto; border-radius: 6px; display: block; margin: 8px 0; }
|
|
7351
7460
|
.extension-detail-section { margin-bottom: 0.875rem; }
|
|
7352
7461
|
.extension-detail-section-title {
|
|
7353
7462
|
font-size: 0.6875rem;
|
|
@@ -11432,12 +11541,11 @@ body.setup-mode[data-theme="dark"] {
|
|
|
11432
11541
|
color: var(--color-danger, #c33);
|
|
11433
11542
|
}
|
|
11434
11543
|
.mcp-tools-header {
|
|
11435
|
-
font-size: 0.
|
|
11544
|
+
font-size: 0.8125rem;
|
|
11436
11545
|
font-weight: 600;
|
|
11437
11546
|
color: var(--color-text-secondary);
|
|
11438
11547
|
margin-bottom: 0.5rem;
|
|
11439
|
-
|
|
11440
|
-
letter-spacing: 0.05em;
|
|
11548
|
+
letter-spacing: 0.01em;
|
|
11441
11549
|
}
|
|
11442
11550
|
.mcp-tool-list {
|
|
11443
11551
|
list-style: none;
|
|
@@ -11449,10 +11557,9 @@ body.setup-mode[data-theme="dark"] {
|
|
|
11449
11557
|
}
|
|
11450
11558
|
.mcp-tool-item {
|
|
11451
11559
|
display: flex;
|
|
11452
|
-
|
|
11453
|
-
gap: 0.
|
|
11454
|
-
|
|
11455
|
-
padding: 0.4375rem 0;
|
|
11560
|
+
flex-direction: column;
|
|
11561
|
+
gap: 0.3rem;
|
|
11562
|
+
padding: 0.625rem 0;
|
|
11456
11563
|
border-bottom: 1px solid var(--color-border-primary, var(--color-border));
|
|
11457
11564
|
}
|
|
11458
11565
|
.mcp-tool-item:last-child {
|
|
@@ -11460,18 +11567,15 @@ body.setup-mode[data-theme="dark"] {
|
|
|
11460
11567
|
}
|
|
11461
11568
|
.mcp-tool-name {
|
|
11462
11569
|
font-family: var(--font-mono);
|
|
11463
|
-
|
|
11464
|
-
|
|
11465
|
-
border-radius: 4px;
|
|
11466
|
-
font-size: 0.75rem;
|
|
11570
|
+
font-size: 0.8125rem;
|
|
11571
|
+
font-weight: 600;
|
|
11467
11572
|
color: var(--color-text-primary, var(--color-text));
|
|
11468
|
-
flex-shrink: 0;
|
|
11469
|
-
border: 1px solid var(--color-border-primary, var(--color-border));
|
|
11470
11573
|
}
|
|
11471
11574
|
.mcp-tool-desc {
|
|
11472
11575
|
color: var(--color-text-secondary);
|
|
11473
|
-
flex: 1;
|
|
11474
11576
|
line-height: 1.5;
|
|
11577
|
+
font-size: 0.8125rem;
|
|
11578
|
+
opacity: 0.6;
|
|
11475
11579
|
}
|
|
11476
11580
|
|
|
11477
11581
|
/* ── MCP CTA banner & action buttons ─────────────────────────────────── */
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
const Onboard = (() => {
|
|
18
18
|
let _providers = [];
|
|
19
19
|
let _selectedLang = I18n.lang(); // language chosen during setup
|
|
20
|
+
let _branded = false; // true when running under a brand license
|
|
20
21
|
|
|
21
22
|
// ── Public API ──────────────────────────────────────────────────────────────
|
|
22
23
|
|
|
@@ -27,6 +28,7 @@ const Onboard = (() => {
|
|
|
27
28
|
if (!data.needs_onboard) return { needsOnboard: false, phase: null };
|
|
28
29
|
|
|
29
30
|
const phase = data.phase;
|
|
31
|
+
_branded = !!data.branded;
|
|
30
32
|
|
|
31
33
|
if (phase === "key_setup") {
|
|
32
34
|
// Mandatory: show full-screen setup panel, block boot.
|
|
@@ -111,9 +113,16 @@ const Onboard = (() => {
|
|
|
111
113
|
$("setup-dot-1").className = "setup-step" + (step === "lang" ? " active" : " done");
|
|
112
114
|
$("setup-dot-2").className = "setup-step" + (step === "key" ? " active" : "");
|
|
113
115
|
if (step === "key") {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
116
|
+
if (_branded) {
|
|
117
|
+
// Brand mode: skip the OpenClacky AI Keys card, go straight to manual config
|
|
118
|
+
$("setup-device-block").style.display = "none";
|
|
119
|
+
$("setup-manual-toggle").style.display = "none";
|
|
120
|
+
$("setup-manual-section").style.display = "";
|
|
121
|
+
} else {
|
|
122
|
+
$("setup-device-block").style.display = "";
|
|
123
|
+
$("setup-manual-toggle").style.display = "";
|
|
124
|
+
$("setup-manual-section").style.display = "none";
|
|
125
|
+
}
|
|
117
126
|
}
|
|
118
127
|
}
|
|
119
128
|
|
|
@@ -379,7 +388,8 @@ const Onboard = (() => {
|
|
|
379
388
|
});
|
|
380
389
|
|
|
381
390
|
$("setup-btn-back").addEventListener("click", () => {
|
|
382
|
-
if ($("setup-manual-section").style.display !== "none") {
|
|
391
|
+
if (!_branded && $("setup-manual-section").style.display !== "none") {
|
|
392
|
+
// Non-brand: collapse manual section back to device card
|
|
383
393
|
$("setup-device-block").style.display = "";
|
|
384
394
|
$("setup-manual-toggle").style.display = "";
|
|
385
395
|
$("setup-manual-section").style.display = "none";
|
|
@@ -58,7 +58,8 @@ const BackupView = (() => {
|
|
|
58
58
|
a.click();
|
|
59
59
|
a.remove();
|
|
60
60
|
URL.revokeObjectURL(url);
|
|
61
|
-
if (statusEl) { statusEl.textContent =
|
|
61
|
+
if (statusEl) { statusEl.textContent = ""; statusEl.className = "model-test-result"; }
|
|
62
|
+
Modal.toast(I18n.t("settings.backup.downloaded"), "success");
|
|
62
63
|
} else if (statusEl) {
|
|
63
64
|
statusEl.textContent = I18n.t("settings.backup.lastError", { msg: res.error });
|
|
64
65
|
statusEl.className = "model-test-result error";
|
|
@@ -108,8 +109,8 @@ const BackupView = (() => {
|
|
|
108
109
|
if (ev.type !== "_ws_connected" || !_restoreStatusEl) return;
|
|
109
110
|
const el = _restoreStatusEl;
|
|
110
111
|
_restoreStatusEl = null;
|
|
111
|
-
el.textContent =
|
|
112
|
-
|
|
112
|
+
if (el) { el.textContent = ""; el.className = "model-test-result"; }
|
|
113
|
+
Modal.toast(I18n.t("settings.backup.restartOk"), "success");
|
|
113
114
|
if (typeof Settings !== "undefined") Settings.open();
|
|
114
115
|
});
|
|
115
116
|
}
|