openclacky 1.5.12 → 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 +48 -0
- data/lib/clacky/access_key.rb +59 -0
- data/lib/clacky/agent/message_compressor_helper.rb +52 -1
- data/lib/clacky/agent/session_serializer.rb +35 -2
- data/lib/clacky/agent/time_machine.rb +9 -1
- data/lib/clacky/agent.rb +11 -7
- data/lib/clacky/agent_config.rb +40 -0
- data/lib/clacky/agent_profile.rb +5 -5
- data/lib/clacky/billing/platform_billing.rb +84 -1
- data/lib/clacky/brand_config.rb +4 -3
- data/lib/clacky/cli.rb +4 -2
- data/lib/clacky/client.rb +107 -0
- data/lib/clacky/default_extensions/ext-studio/api/handler.rb +3 -0
- data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +40 -21
- data/lib/clacky/default_extensions/git/ext.yml +1 -1
- data/lib/clacky/default_extensions/git/panels/git/view.js +483 -77
- 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/verifier.rb +14 -1
- 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 +6 -2
- data/lib/clacky/openai_responses_stream_aggregator.rb +294 -0
- data/lib/clacky/providers.rb +18 -6
- 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 +147 -103
- data/lib/clacky/tools/terminal.rb +27 -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/utils/model_pricing.rb +70 -4
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +412 -108
- data/lib/clacky/web/components/composer.js +45 -7
- data/lib/clacky/web/components/custom-select.js +136 -0
- data/lib/clacky/web/components/mentions.js +58 -2
- data/lib/clacky/web/components/model-picker.js +527 -0
- data/lib/clacky/web/components/onboard.js +29 -55
- data/lib/clacky/web/core/ext.js +38 -0
- data/lib/clacky/web/features/billing/view.js +20 -6
- 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 +31 -22
- 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 +54 -3
- data/lib/clacky/web/i18n.js +73 -11
- data/lib/clacky/web/index.html +92 -37
- data/lib/clacky/web/sessions.js +550 -592
- data/lib/clacky/web/settings.js +163 -190
- data/lib/clacky.rb +3 -0
- metadata +8 -1
data/lib/clacky/client.rb
CHANGED
|
@@ -44,6 +44,7 @@ module Clacky
|
|
|
44
44
|
effective_api_format ||= "anthropic-messages" if anthropic_format
|
|
45
45
|
resolved_type = Providers.api_type_for_model(provider_id, @model, user_override: effective_api_format)
|
|
46
46
|
@use_anthropic_format = resolved_type == "anthropic-messages"
|
|
47
|
+
@use_responses_format = resolved_type == "openai-responses"
|
|
47
48
|
|
|
48
49
|
# Remember the provider id so we can tune connection headers below
|
|
49
50
|
# (OpenRouter's /v1/messages accepts either Bearer or x-api-key, but
|
|
@@ -66,6 +67,12 @@ module Clacky
|
|
|
66
67
|
@use_anthropic_format && !@use_bedrock
|
|
67
68
|
end
|
|
68
69
|
|
|
70
|
+
# Returns true when the client talks to the OpenAI Responses API
|
|
71
|
+
# (/v1/responses) instead of Chat Completions.
|
|
72
|
+
def responses_format?(model = nil)
|
|
73
|
+
@use_responses_format && !@use_bedrock
|
|
74
|
+
end
|
|
75
|
+
|
|
69
76
|
# ── Connection test ───────────────────────────────────────────────────────
|
|
70
77
|
|
|
71
78
|
# Test API connection by sending a minimal request.
|
|
@@ -81,6 +88,11 @@ module Clacky
|
|
|
81
88
|
minimal_body = { model: api_model, max_tokens: 16,
|
|
82
89
|
messages: [{ role: "user", content: "hi" }] }.to_json
|
|
83
90
|
response = anthropic_connection.post(anthropic_messages_path) { |r| r.body = minimal_body }
|
|
91
|
+
elsif responses_format?
|
|
92
|
+
minimal_body = MessageFormat::OpenAIResponses.build_request_body(
|
|
93
|
+
[{ role: "user", content: "hi" }], api_model, [], 16, false
|
|
94
|
+
).to_json
|
|
95
|
+
response = openai_connection.post("responses") { |r| r.body = minimal_body }
|
|
84
96
|
else
|
|
85
97
|
minimal_body = { model: api_model, max_tokens: 16,
|
|
86
98
|
messages: [{ role: "user", content: "hi" }] }.to_json
|
|
@@ -113,6 +125,10 @@ module Clacky
|
|
|
113
125
|
body = MessageFormat::Anthropic.build_request_body(messages, api_model, [], max_tokens, false)
|
|
114
126
|
response = anthropic_connection.post(anthropic_messages_path) { |r| r.body = body.to_json }
|
|
115
127
|
parse_simple_anthropic_response(response)
|
|
128
|
+
elsif responses_format?
|
|
129
|
+
body = MessageFormat::OpenAIResponses.build_request_body(messages, api_model, [], max_tokens, false)
|
|
130
|
+
response = openai_connection.post("responses") { |r| r.body = body.to_json }
|
|
131
|
+
parse_simple_openai_responses_response(response)
|
|
116
132
|
else
|
|
117
133
|
body = MessageFormat::OpenAI.build_request_body(messages, api_model, [], max_tokens, false, reasoning_effort: reasoning_effort)
|
|
118
134
|
response = openai_connection.post("chat/completions") { |r| r.body = body.to_json }
|
|
@@ -164,6 +180,9 @@ module Clacky
|
|
|
164
180
|
elsif anthropic_format?
|
|
165
181
|
streaming_used = !on_chunk.nil?
|
|
166
182
|
send_anthropic_request(cloned, api_model, tools, max_tokens, caching_enabled, reasoning_effort: reasoning_effort, on_chunk: wrapped_on_chunk)
|
|
183
|
+
elsif responses_format?
|
|
184
|
+
streaming_used = !on_chunk.nil?
|
|
185
|
+
send_openai_responses_request(cloned, api_model, tools, max_tokens, caching_enabled, reasoning_effort: reasoning_effort, on_chunk: wrapped_on_chunk, capability_model: model)
|
|
167
186
|
else
|
|
168
187
|
streaming_used = !on_chunk.nil?
|
|
169
188
|
send_openai_request(cloned, api_model, tools, max_tokens, caching_enabled, reasoning_effort: reasoning_effort, on_chunk: wrapped_on_chunk, capability_model: model)
|
|
@@ -206,6 +225,8 @@ module Clacky
|
|
|
206
225
|
MessageFormat::Bedrock.format_tool_results(response, tool_results)
|
|
207
226
|
elsif anthropic_format?
|
|
208
227
|
MessageFormat::Anthropic.format_tool_results(response, tool_results)
|
|
228
|
+
elsif responses_format?
|
|
229
|
+
MessageFormat::OpenAIResponses.format_tool_results(response, tool_results)
|
|
209
230
|
else
|
|
210
231
|
MessageFormat::OpenAI.format_tool_results(response, tool_results)
|
|
211
232
|
end
|
|
@@ -460,6 +481,92 @@ module Clacky
|
|
|
460
481
|
content
|
|
461
482
|
end
|
|
462
483
|
|
|
484
|
+
# ── OpenAI Responses API request / response ───────────────────────────────
|
|
485
|
+
|
|
486
|
+
def send_openai_responses_request(messages, model, tools, max_tokens, caching_enabled,
|
|
487
|
+
reasoning_effort: nil, on_chunk: nil, capability_model: nil)
|
|
488
|
+
# Override max_tokens when the model declares a higher output ceiling
|
|
489
|
+
model_for_limit = capability_model || model
|
|
490
|
+
model_limit = Providers.max_output_for(model_for_limit)
|
|
491
|
+
max_tokens = model_limit if model_limit
|
|
492
|
+
|
|
493
|
+
# Deliberately no apply_message_caching here: the Responses API does
|
|
494
|
+
# not recognize Anthropic-style cache_control markers, and OpenAI's
|
|
495
|
+
# Responses prompt caching is automatic server-side. Injecting
|
|
496
|
+
# cache_control would be silently ignored (or rejected by stricter
|
|
497
|
+
# endpoints).
|
|
498
|
+
|
|
499
|
+
cap_model = capability_model || model
|
|
500
|
+
body = MessageFormat::OpenAIResponses.build_request_body(
|
|
501
|
+
messages, model, tools, max_tokens, caching_enabled,
|
|
502
|
+
vision_supported: Providers.supports?(@provider_id, :vision, model_name: cap_model),
|
|
503
|
+
reasoning_effort: reasoning_effort
|
|
504
|
+
)
|
|
505
|
+
return send_openai_responses_stream_request(body, on_chunk) if on_chunk
|
|
506
|
+
|
|
507
|
+
response = openai_connection.post("responses") { |r| r.body = body.to_json }
|
|
508
|
+
|
|
509
|
+
raise_error(response) unless response.status == 200
|
|
510
|
+
check_html_response(response)
|
|
511
|
+
|
|
512
|
+
parsed_body = safe_json_parse(response.body, context: "LLM response")
|
|
513
|
+
MessageFormat::OpenAIResponses.parse_response(parsed_body)
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
# Streaming variant for the OpenAI Responses API.
|
|
517
|
+
# Posts to the "responses" endpoint with stream:true; the upstream returns
|
|
518
|
+
# typed SSE events (response.output_text.delta,
|
|
519
|
+
# response.function_call_arguments.delta, response.completed, etc.) that
|
|
520
|
+
# the aggregator reassembles into the non-streaming response shape.
|
|
521
|
+
private def send_openai_responses_stream_request(body, on_chunk)
|
|
522
|
+
stream_body = body.merge(stream: true)
|
|
523
|
+
aggregator = OpenAIResponsesStreamAggregator.new(on_chunk: on_chunk)
|
|
524
|
+
sse_buf = +""
|
|
525
|
+
|
|
526
|
+
response = openai_connection.post("responses") do |req|
|
|
527
|
+
req.headers["Accept"] = "text/event-stream"
|
|
528
|
+
req.body = stream_body.to_json
|
|
529
|
+
req.options.on_data = proc do |chunk, _bytes_received, _env|
|
|
530
|
+
sse_buf << chunk
|
|
531
|
+
drain_sse_frames(sse_buf) { |_event, data| aggregator.handle(data) }
|
|
532
|
+
end
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
unless response.status == 200
|
|
536
|
+
response.env.body = sse_buf if response.body.to_s.empty?
|
|
537
|
+
raise_error(response)
|
|
538
|
+
end
|
|
539
|
+
|
|
540
|
+
result = aggregator.to_h
|
|
541
|
+
log_stream_summary("openai-responses", aggregator, aggregator.saw_done? ? "completed" : nil)
|
|
542
|
+
# A complete Responses API stream always terminates with a
|
|
543
|
+
# response.completed / response.done (or response.incomplete) event.
|
|
544
|
+
# Its absence means the upstream cut the stream mid-response; retry
|
|
545
|
+
# rather than accept a silently truncated answer.
|
|
546
|
+
unless aggregator.saw_done?
|
|
547
|
+
raise Clacky::UpstreamTruncatedError,
|
|
548
|
+
"[LLM] Streaming response ended without response.completed (upstream cut the stream). Retrying..."
|
|
549
|
+
end
|
|
550
|
+
MessageFormat::OpenAIResponses.parse_response(result)
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
def parse_simple_openai_responses_response(response)
|
|
554
|
+
raise_error(response) unless response.status == 200
|
|
555
|
+
parsed_body = safe_json_parse(response.body, context: "LLM response")
|
|
556
|
+
result = MessageFormat::OpenAIResponses.parse_response(parsed_body)
|
|
557
|
+
content = result[:content]
|
|
558
|
+
if content.nil?
|
|
559
|
+
snippet = response.body.to_s[0, 1200]
|
|
560
|
+
if defined?(Clacky::Logger)
|
|
561
|
+
Clacky::Logger.warn("[parse_simple_openai_responses_response] no content. status=#{response.status} body=#{snippet}")
|
|
562
|
+
end
|
|
563
|
+
raise RetryableError,
|
|
564
|
+
"Upstream Responses API response missing text content. " \
|
|
565
|
+
"Body snippet: #{snippet}"
|
|
566
|
+
end
|
|
567
|
+
content
|
|
568
|
+
end
|
|
569
|
+
|
|
463
570
|
# ── Prompt caching helpers ────────────────────────────────────────────────
|
|
464
571
|
|
|
465
572
|
# Add cache_control markers to the last 2 messages in the array.
|
|
@@ -344,6 +344,9 @@ class ExtStudioExt < Clacky::ApiExtension
|
|
|
344
344
|
ext_id = require_ext_id!
|
|
345
345
|
version = presence(json_body["version"])
|
|
346
346
|
error!("version required", status: 422) unless version
|
|
347
|
+
unless Clacky::ExtensionVerifier.valid_version?(version)
|
|
348
|
+
error!("version must use three numeric segments (for example, 1.0.0)", status: 422)
|
|
349
|
+
end
|
|
347
350
|
|
|
348
351
|
result = Clacky::ExtensionLoader.load_all(force: false)
|
|
349
352
|
container = Array(result.containers).find { |id, _| id == ext_id }&.last
|
|
@@ -627,8 +627,12 @@
|
|
|
627
627
|
}
|
|
628
628
|
}
|
|
629
629
|
|
|
630
|
+
function normalizeVersion(v) {
|
|
631
|
+
return String(v || "").replace(/[^0-9.]/g, "").split(".").slice(0, 3).join(".");
|
|
632
|
+
}
|
|
633
|
+
|
|
630
634
|
function isSemver(v) {
|
|
631
|
-
return
|
|
635
|
+
return /^[0-9]+\.[0-9]+\.[0-9]+$/.test(v);
|
|
632
636
|
}
|
|
633
637
|
|
|
634
638
|
// Unified publish flow: one modal that walks the creator from a release form
|
|
@@ -642,23 +646,14 @@
|
|
|
642
646
|
const titleNew = isBrand ? t("pub.brand.title.new") : t("pub.title.new");
|
|
643
647
|
const titleUpdate = isBrand ? t("pub.brand.title.update") : t("pub.title.update");
|
|
644
648
|
const introText = isBrand ? t("pub.brand.intro", { name: ext.name }) : t("pub.intro", { name: ext.name });
|
|
645
|
-
let currentVersion = ext.version
|
|
649
|
+
let currentVersion = normalizeVersion(ext.version);
|
|
646
650
|
let isUpdate = false;
|
|
647
651
|
|
|
648
652
|
const verField = el("div", { class: "studio-field" });
|
|
649
653
|
verField.appendChild(el("label", { class: "studio-label", text: t("pub.version.label") }));
|
|
650
|
-
const verInput = el("input", {
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
const valid = isSemver(currentVersion);
|
|
654
|
-
verInput.classList.toggle("studio-input-error", !!currentVersion && !valid);
|
|
655
|
-
publishBtn.disabled = !valid;
|
|
656
|
-
publishBtn.textContent = valid ? t("pub.btn.publish", { ver: currentVersion }) : t("pub.btn.publish", { ver: currentVersion || "?" });
|
|
657
|
-
if (currentVersion && !valid) {
|
|
658
|
-
setProgress(t("err.invalid_version"), true);
|
|
659
|
-
} else {
|
|
660
|
-
status.style.display = "none";
|
|
661
|
-
}
|
|
654
|
+
const verInput = el("input", {
|
|
655
|
+
class: "studio-input", type: "text", value: currentVersion,
|
|
656
|
+
placeholder: "1.0.0", inputmode: "decimal", autocomplete: "off", spellcheck: "false",
|
|
662
657
|
});
|
|
663
658
|
verField.appendChild(verInput);
|
|
664
659
|
|
|
@@ -694,7 +689,8 @@
|
|
|
694
689
|
|
|
695
690
|
const publishBtn = modal.footer.querySelector(".btn-primary");
|
|
696
691
|
const cancelBtn = modal.footer.querySelector(".btn-secondary");
|
|
697
|
-
|
|
692
|
+
verInput.addEventListener("input", () => syncVersionState({ normalize: true }));
|
|
693
|
+
syncVersionState();
|
|
698
694
|
|
|
699
695
|
Promise.resolve(prevVersionOrPromise).then((prevVersion) => {
|
|
700
696
|
if (done || !prevVersion) return;
|
|
@@ -702,10 +698,8 @@
|
|
|
702
698
|
const titleEl = modal.overlay.querySelector(".modal-title");
|
|
703
699
|
if (titleEl) titleEl.textContent = titleUpdate;
|
|
704
700
|
if (prevVersion !== currentVersion) {
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
publishBtn.disabled = false;
|
|
708
|
-
publishBtn.textContent = t("pub.btn.publish", { ver: currentVersion });
|
|
701
|
+
verInput.value = normalizeVersion(prevVersion);
|
|
702
|
+
syncVersionState();
|
|
709
703
|
}
|
|
710
704
|
});
|
|
711
705
|
|
|
@@ -715,9 +709,34 @@
|
|
|
715
709
|
status.className = "studio-modal-status" + (isError ? " studio-modal-status-error" : "");
|
|
716
710
|
}
|
|
717
711
|
|
|
712
|
+
function syncVersionState({ normalize = false, updateFeedback = true } = {}) {
|
|
713
|
+
if (normalize) {
|
|
714
|
+
const raw = verInput.value;
|
|
715
|
+
const cursor = verInput.selectionStart;
|
|
716
|
+
const normalized = normalizeVersion(raw);
|
|
717
|
+
if (normalized !== raw) {
|
|
718
|
+
verInput.value = normalized;
|
|
719
|
+
if (cursor != null) {
|
|
720
|
+
const normalizedCursor = normalizeVersion(raw.slice(0, cursor)).length;
|
|
721
|
+
verInput.setSelectionRange(normalizedCursor, normalizedCursor);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
currentVersion = verInput.value.trim();
|
|
727
|
+
const valid = isSemver(currentVersion);
|
|
728
|
+
publishBtn.disabled = !valid;
|
|
729
|
+
publishBtn.textContent = t("pub.btn.publish", { ver: valid ? currentVersion : (currentVersion || "?") });
|
|
730
|
+
|
|
731
|
+
if (updateFeedback) {
|
|
732
|
+
verInput.classList.toggle("studio-input-error", !!currentVersion && !valid);
|
|
733
|
+
if (currentVersion && !valid) setProgress(t("err.invalid_version"), true);
|
|
734
|
+
else status.style.display = "none";
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
718
738
|
function resetButtons() {
|
|
719
|
-
|
|
720
|
-
publishBtn.textContent = currentVersion ? t("pub.btn.publish", { ver: currentVersion }) : t("pub.btn.publish", { ver: "?" });
|
|
739
|
+
syncVersionState({ updateFeedback: false });
|
|
721
740
|
cancelBtn.disabled = false;
|
|
722
741
|
notes.disabled = false;
|
|
723
742
|
verInput.disabled = false;
|