openclacky 1.5.21 → 1.5.22

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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +61 -0
  3. data/lib/clacky/billing/platform_billing.rb +3 -0
  4. data/lib/clacky/default_extensions/git/panels/git/view.js +1911 -398
  5. data/lib/clacky/default_extensions/preview/panels/preview/view.js +199 -34
  6. data/lib/clacky/providers.rb +43 -17
  7. data/lib/clacky/search_config.rb +33 -11
  8. data/lib/clacky/server/git_panel.rb +360 -34
  9. data/lib/clacky/server/http_server.rb +145 -47
  10. data/lib/clacky/server/preview.rb +118 -17
  11. data/lib/clacky/server/scheduler.rb +81 -2
  12. data/lib/clacky/server/web_ui_controller.rb +35 -27
  13. data/lib/clacky/session_manager.rb +52 -5
  14. data/lib/clacky/utils/mac_app_detector.rb +8 -7
  15. data/lib/clacky/utils/model_pricing.rb +60 -2
  16. data/lib/clacky/utils/windows_app_detector.rb +3 -1
  17. data/lib/clacky/version.rb +1 -1
  18. data/lib/clacky/web/app.css +650 -96
  19. data/lib/clacky/web/app.js +59 -6
  20. data/lib/clacky/web/auth.js +1 -1
  21. data/lib/clacky/web/components/composer.js +107 -8
  22. data/lib/clacky/web/components/image-zoom.js +165 -0
  23. data/lib/clacky/web/components/model-picker.js +6 -4
  24. data/lib/clacky/web/components/notify.js +2 -2
  25. data/lib/clacky/web/core/aside.js +60 -3
  26. data/lib/clacky/web/features/billing/view.js +2 -2
  27. data/lib/clacky/web/features/channels/view.js +5 -2
  28. data/lib/clacky/web/features/extensions/view.js +2 -2
  29. data/lib/clacky/web/features/new-session/store.js +79 -2
  30. data/lib/clacky/web/features/new-session/view.js +496 -58
  31. data/lib/clacky/web/features/skills/store.js +1 -1
  32. data/lib/clacky/web/features/tasks/view.js +162 -51
  33. data/lib/clacky/web/features/workspace/store.js +13 -6
  34. data/lib/clacky/web/features/workspace/view.js +153 -163
  35. data/lib/clacky/web/i18n.js +238 -56
  36. data/lib/clacky/web/index.html +173 -157
  37. data/lib/clacky/web/projects.js +37 -4
  38. data/lib/clacky/web/sessions.js +181 -105
  39. data/lib/clacky/web/settings.js +26 -8
  40. data/lib/clacky/web/skills.js +2 -2
  41. data/lib/clacky/web/theme.js +17 -2
  42. data/lib/clacky/web/utils.js +17 -2
  43. data/lib/clacky/web/weixin-qr.html +2 -1
  44. data/lib/clacky/web/ws-dispatcher.js +8 -29
  45. metadata +2 -1
@@ -30,6 +30,9 @@ const Settings = (() => {
30
30
  // Last successfully activated tab. Dynamic extension tabs may be rebuilt
31
31
  // while Settings is hidden, so the DOM alone cannot retain this state.
32
32
  let _activeTab = "models";
33
+ // Masked web-search keys per provider. Module-level because the provider
34
+ // select and save handlers outlive the initializer that first bound them.
35
+ let _searchKeysMasked = {};
33
36
 
34
37
  // ── Public API ──────────────────────────────────────────────────────────────
35
38
 
@@ -272,16 +275,20 @@ const Settings = (() => {
272
275
 
273
276
  card.innerHTML = `
274
277
  <div class="model-card-grid-info">
275
- <div class="model-card-grid-name-row">
276
- <span class="model-card-grid-name">${_esc(displayName)}</span>
277
- ${isDefault ? `<span class="badge badge-default">${I18n.t("settings.models.badge.default")}</span>` : ""}
278
- ${isLite ? `<span class="badge badge-lite">${I18n.t("settings.models.badge.lite")}</span>` : ""}
279
- ${!isDefault ? `<button class="btn-card-grid-action btn-card-grid-action-primary" data-index="${index}" data-action="default" style="margin-left:auto">
278
+ <div class="model-card-grid-head">
279
+ <div class="model-card-grid-head-text">
280
+ <div class="model-card-grid-name-row">
281
+ <span class="model-card-grid-name">${_esc(displayName)}</span>
282
+ ${isDefault ? `<span class="badge badge-default">${I18n.t("settings.models.badge.default")}</span>` : ""}
283
+ ${isLite ? `<span class="badge badge-lite">${I18n.t("settings.models.badge.lite")}</span>` : ""}
284
+ </div>
285
+ <div class="model-card-grid-provider">${_esc(providerName)}</div>
286
+ </div>
287
+ ${!isDefault ? `<button class="btn-card-grid-action btn-card-grid-action-primary" data-index="${index}" data-action="default">
280
288
  <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>
281
289
  <span>${I18n.t("settings.models.btn.setDefault")}</span>
282
290
  </button>` : ""}
283
291
  </div>
284
- <div class="model-card-grid-provider">${_esc(providerName)}</div>
285
292
  ${model.api_key_masked ? `<div class="model-card-grid-model">${_esc(model.api_key_masked)}</div>` : ""}
286
293
  ${remark ? `<div class="model-card-grid-remark" title="${_esc(remark)}">
287
294
  <span class="model-card-grid-remark-label">${I18n.t("settings.models.field.remark")}</span>
@@ -320,6 +327,7 @@ const Settings = (() => {
320
327
  </div>
321
328
  `;
322
329
 
330
+ card.querySelector(".model-card-grid-head").prepend(ModelPicker.vendorBadge(displayName));
323
331
  container.appendChild(card);
324
332
  _bindCompactCardEvents(card, index);
325
333
  }
@@ -1788,7 +1796,10 @@ const Settings = (() => {
1788
1796
  select = CustomSelect.init({
1789
1797
  trigger: trigger,
1790
1798
  dropdown: dropdown,
1791
- onSelect: function () { _syncKeyVisibility(); }
1799
+ onSelect: function (value) {
1800
+ if (keyInput) keyInput.value = _searchKeysMasked[value] || "";
1801
+ _syncKeyVisibility();
1802
+ }
1792
1803
  });
1793
1804
  }
1794
1805
 
@@ -1861,8 +1872,9 @@ const Settings = (() => {
1861
1872
  `<div class="custom-select-option" data-value="">${I18n.t("settings.search.builtin")}</div>` +
1862
1873
  deepseekRow +
1863
1874
  rows;
1875
+ _searchKeysMasked = data.keys_masked || {};
1864
1876
  if (select) select.setValue(data.provider || "");
1865
- keyInput.value = data.key_masked || "";
1877
+ keyInput.value = _searchKeysMasked[data.provider] || "";
1866
1878
  }
1867
1879
  } catch (_) { /* non-critical */ }
1868
1880
  _syncKeyVisibility();
@@ -1896,6 +1908,7 @@ const Settings = (() => {
1896
1908
  });
1897
1909
  const data = await res.json();
1898
1910
  if (data.ok) {
1911
+ _searchKeysMasked = data.keys_masked || {};
1899
1912
  Modal.toast(I18n.t("settings.search.saved"), "success");
1900
1913
  } else {
1901
1914
  _showStatus(data.error || I18n.t("settings.search.failed"), false);
@@ -2342,6 +2355,11 @@ const Settings = (() => {
2342
2355
  btn.classList.toggle("active", btn.dataset.lang === I18n.lang());
2343
2356
  btn.addEventListener("click", () => {
2344
2357
  I18n.setLang(btn.dataset.lang);
2358
+ // applyAll only rewrites static attributes; the info bar composes its
2359
+ // tooltips in JS, so refresh them once for the new language.
2360
+ if (typeof Sessions !== "undefined" && Sessions._lastSession) {
2361
+ Sessions.updateInfoBar(Sessions._lastSession);
2362
+ }
2345
2363
  document.querySelectorAll("#language-section .settings-lang-btn").forEach(b =>
2346
2364
  b.classList.toggle("active", b.dataset.lang === I18n.lang())
2347
2365
  );
@@ -18,8 +18,8 @@
18
18
  // Depends on: Sessions (sendMessage), Skills (createInSession, toggleImportBar),
19
19
  // I18n, global $ helper.
20
20
  // ─────────────────────────────────────────────────────────────────────────
21
- const SKILL_AC_SHOW_SYSTEM_KEY = "skill-ac-show-system";
22
- const SKILL_AC_MRU_KEY = "skill-ac-mru";
21
+ const SKILL_AC_SHOW_SYSTEM_KEY = "clacky-skill-ac-show-system";
22
+ const SKILL_AC_MRU_KEY = "clacky-skill-ac-mru";
23
23
 
24
24
  const SkillAC = (() => {
25
25
  let _initialized = false;
@@ -28,6 +28,21 @@ const Theme = (() => {
28
28
  window.dispatchEvent(new CustomEvent("clacky-theme-change", { detail: { theme } }));
29
29
  }
30
30
 
31
+ // Per-element transitions have different durations, so a live switch would
32
+ // repaint regions out of step; freeze them and cross-fade the page as one.
33
+ function _switchTo(theme) {
34
+ const root = document.documentElement;
35
+ const swap = () => {
36
+ root.classList.add("theme-switching");
37
+ _applyAttr(theme);
38
+ void root.offsetWidth;
39
+ requestAnimationFrame(() => root.classList.remove("theme-switching"));
40
+ };
41
+ const reduceMotion = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
42
+ if (document.startViewTransition && !reduceMotion) document.startViewTransition(swap);
43
+ else swap();
44
+ }
45
+
31
46
  // Update active state on all .settings-bg-theme-card buttons.
32
47
  function _syncBgCards() {
33
48
  const effective = current();
@@ -45,7 +60,7 @@ const Theme = (() => {
45
60
  const mq = window.matchMedia("(prefers-color-scheme: dark)");
46
61
  const onChange = () => {
47
62
  if (!localStorage.getItem(STORAGE_KEY)) {
48
- _applyAttr(_systemTheme());
63
+ _switchTo(_systemTheme());
49
64
  }
50
65
  };
51
66
  if (mq.addEventListener) mq.addEventListener("change", onChange);
@@ -61,7 +76,7 @@ const Theme = (() => {
61
76
  } else {
62
77
  localStorage.setItem(STORAGE_KEY, theme);
63
78
  }
64
- _applyAttr(theme);
79
+ _switchTo(theme);
65
80
  }
66
81
 
67
82
  // Keep applyBg as alias for backward compat (settings.js calls it)
@@ -133,6 +133,10 @@ const Tooltip = (() => {
133
133
  tip.textContent = text;
134
134
  tip.setAttribute("data-pos", pos);
135
135
  tip.style.display = "block";
136
+ // Measure from the left edge: leaving the previous anchor's `left` in place
137
+ // shrinks the available width, so a tip next to the right edge wraps early.
138
+ tip.style.left = "0px";
139
+ tip.style.top = "0px";
136
140
 
137
141
  const r = anchor.getBoundingClientRect();
138
142
  const tw = tip.offsetWidth;
@@ -168,16 +172,27 @@ const Tooltip = (() => {
168
172
  _hideTimer = setTimeout(() => { tip.style.display = "none"; }, 120);
169
173
  }
170
174
 
175
+ // Anchor that was clicked; stays silent until the pointer leaves it so the
176
+ // tip does not cover the menu or popover the click just opened.
177
+ let _suppressed = null;
178
+
171
179
  function init() {
172
180
  document.addEventListener("mouseover", (e) => {
173
181
  const anchor = e.target.closest("[data-tooltip]");
174
- if (anchor) show(anchor);
182
+ if (anchor && anchor !== _suppressed) show(anchor);
175
183
  });
176
184
  document.addEventListener("mouseout", (e) => {
177
185
  const anchor = e.target.closest("[data-tooltip]");
178
186
  if (!anchor) return;
179
- if (!anchor.contains(e.relatedTarget)) hide();
187
+ if (anchor.contains(e.relatedTarget)) return;
188
+ if (anchor === _suppressed) _suppressed = null;
189
+ hide();
180
190
  });
191
+ document.addEventListener("pointerdown", (e) => {
192
+ const anchor = e.target.closest && e.target.closest("[data-tooltip]");
193
+ _suppressed = anchor || null;
194
+ hide();
195
+ }, true);
181
196
  }
182
197
 
183
198
  return { init, show, hide };
@@ -1,8 +1,9 @@
1
1
  <!DOCTYPE html>
2
- <html lang="zh">
2
+ <html lang="zh" translate="no">
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <meta name="google" content="notranslate">
6
7
  <title>微信扫码登录</title>
7
8
  <style>
8
9
  * { box-sizing: border-box; margin: 0; padding: 0; }
@@ -393,7 +393,6 @@ function _dispatchEvent(ev) {
393
393
  Skills.load();
394
394
  // Clear progress state for this session (even if not currently active)
395
395
  Sessions.clearProgress(sid);
396
- if (patch.status === "idle") Sessions.markDone(sid);
397
396
  Sessions.renderList();
398
397
  }
399
398
  break;
@@ -405,6 +404,13 @@ function _dispatchEvent(ev) {
405
404
  // module decides whether the user is looking at that session.
406
405
  case "task_finished":
407
406
  if (typeof Notify !== "undefined") Notify.onTaskFinished(ev.session_id, ev.awaiting_feedback);
407
+ // Only a real finish lights the done dot; idle snapshots (moves, renames, compression) must not.
408
+ if (!ev.awaiting_feedback) {
409
+ Sessions.markDone(ev.session_id);
410
+ Sessions.renderList();
411
+ Projects.renderSection();
412
+ }
413
+ if (ev.complete) Sessions.markDoneSummary(ev.session_id, ev.complete);
408
414
  break;
409
415
 
410
416
  case "session_renamed": {
@@ -633,34 +639,7 @@ function _dispatchEvent(ev) {
633
639
  Sessions.clearProgress();
634
640
  Sessions.collapseToolGroup();
635
641
  _closeAllPhases("incomplete"); // safety net: missed phase_end
636
- {
637
- const costSource = ev.cost_source;
638
- const symbol = typeof Billing !== "undefined" ? Billing.getCurrencySymbol() : "$";
639
- const rawCost = ev.cost || 0;
640
- const cost = typeof Billing !== "undefined" ? Billing.convertCost(rawCost) : rawCost;
641
- const costDisplay = (!costSource || costSource === "estimated")
642
- ? "N/A"
643
- : `${symbol}${cost.toFixed(4)}`;
644
- let mainLine = I18n.t("chat.done", { n: ev.iterations, cost: costDisplay });
645
- if (typeof ev.duration === "number" && ev.duration > 0) {
646
- mainLine += I18n.t("chat.done.duration", { duration: ev.duration.toFixed(1) });
647
- }
648
- let cacheLine = null;
649
- const cs = ev.cache_stats;
650
- const total = cs && (cs.total_requests || cs["total_requests"]);
651
- const hits = cs && (cs.cache_hit_requests || cs["cache_hit_requests"]);
652
- const cachedTokens = cs && (cs.cache_read_input_tokens || cs["cache_read_input_tokens"]);
653
- if (total && total > 0 && cachedTokens && cachedTokens > 0) {
654
- const rate = ((hits / total) * 100).toFixed(1);
655
- const tokensFmt = cachedTokens >= 1000
656
- ? `${(cachedTokens / 1000).toFixed(1)}k`
657
- : `${cachedTokens}`;
658
- cacheLine = I18n.t("chat.done.cache", {
659
- rate, hits, total: total, tokens: tokensFmt
660
- });
661
- }
662
- Sessions.appendInfo(`✓ ${mainLine}`, cacheLine);
663
- }
642
+ Sessions.appendCompleteSummary(ev);
664
643
  if (typeof Share !== "undefined") Share.maybePromptOnComplete();
665
644
  break;
666
645
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: openclacky
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.21
4
+ version: 1.5.22
5
5
  platform: ruby
6
6
  authors:
7
7
  - windy
@@ -688,6 +688,7 @@ files:
688
688
  - lib/clacky/web/components/composer.js
689
689
  - lib/clacky/web/components/custom-select.js
690
690
  - lib/clacky/web/components/datepicker.js
691
+ - lib/clacky/web/components/image-zoom.js
691
692
  - lib/clacky/web/components/mentions.js
692
693
  - lib/clacky/web/components/model-picker.js
693
694
  - lib/clacky/web/components/notify.js