openclacky 1.2.10 → 1.2.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.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/.clacky/skills/gem-release/SKILL.md +1 -1
  3. data/.clacky/skills/gem-release/scripts/release.sh +4 -1
  4. data/CHANGELOG.md +56 -1
  5. data/lib/clacky/agent/llm_caller.rb +40 -25
  6. data/lib/clacky/agent/memory_updater.rb +12 -0
  7. data/lib/clacky/agent/session_serializer.rb +1 -1
  8. data/lib/clacky/agent/skill_auto_creator.rb +7 -4
  9. data/lib/clacky/agent/skill_evolution.rb +23 -5
  10. data/lib/clacky/agent/skill_manager.rb +86 -1
  11. data/lib/clacky/agent/skill_reflector.rb +18 -23
  12. data/lib/clacky/agent/tool_registry.rb +10 -0
  13. data/lib/clacky/agent.rb +68 -23
  14. data/lib/clacky/agent_config.rb +59 -15
  15. data/lib/clacky/anthropic_stream_aggregator.rb +17 -1
  16. data/lib/clacky/bedrock_stream_aggregator.rb +17 -1
  17. data/lib/clacky/cli.rb +55 -0
  18. data/lib/clacky/client.rb +25 -3
  19. data/lib/clacky/default_skills/channel-manager/SKILL.md +47 -42
  20. data/lib/clacky/default_skills/channel-manager/feishu_setup.rb +134 -0
  21. data/lib/clacky/default_skills/media-gen/SKILL.md +5 -0
  22. data/lib/clacky/default_skills/persist-memory/SKILL.md +4 -3
  23. data/lib/clacky/default_skills/search-skills/SKILL.md +61 -0
  24. data/lib/clacky/idle_compression_timer.rb +1 -1
  25. data/lib/clacky/message_format/open_ai.rb +7 -1
  26. data/lib/clacky/message_history.rb +57 -0
  27. data/lib/clacky/openai_stream_aggregator.rb +30 -3
  28. data/lib/clacky/providers.rb +40 -12
  29. data/lib/clacky/server/channel/adapters/dingtalk/adapter.rb +10 -1
  30. data/lib/clacky/server/channel/adapters/discord/adapter.rb +8 -2
  31. data/lib/clacky/server/channel/adapters/feishu/adapter.rb +10 -1
  32. data/lib/clacky/server/channel/adapters/feishu/bot.rb +12 -0
  33. data/lib/clacky/server/channel/adapters/feishu/message_parser.rb +23 -3
  34. data/lib/clacky/server/channel/adapters/telegram/adapter.rb +12 -2
  35. data/lib/clacky/server/channel/adapters/wecom/adapter.rb +5 -1
  36. data/lib/clacky/server/channel/channel_manager.rb +65 -4
  37. data/lib/clacky/server/channel/group_message_buffer.rb +53 -0
  38. data/lib/clacky/server/http_server.rb +190 -10
  39. data/lib/clacky/server/session_registry.rb +34 -14
  40. data/lib/clacky/server/web_ui_controller.rb +24 -1
  41. data/lib/clacky/session_manager.rb +120 -0
  42. data/lib/clacky/tools/trash_manager.rb +1 -1
  43. data/lib/clacky/tools/web_search.rb +59 -8
  44. data/lib/clacky/ui2/layout_manager.rb +15 -5
  45. data/lib/clacky/ui2/progress_handle.rb +7 -1
  46. data/lib/clacky/ui2/ui_controller.rb +27 -0
  47. data/lib/clacky/ui_interface.rb +22 -0
  48. data/lib/clacky/utils/model_pricing.rb +96 -0
  49. data/lib/clacky/version.rb +1 -1
  50. data/lib/clacky/web/app.css +230 -7
  51. data/lib/clacky/web/app.js +6 -5
  52. data/lib/clacky/web/apple-touch-icon-180.png +0 -0
  53. data/lib/clacky/web/brand.js +22 -2
  54. data/lib/clacky/web/favicon.ico +0 -0
  55. data/lib/clacky/web/i18n.js +22 -4
  56. data/lib/clacky/web/index.html +6 -4
  57. data/lib/clacky/web/logo_nav_dark.png +0 -0
  58. data/lib/clacky/web/model-tester.js +8 -1
  59. data/lib/clacky/web/sessions.js +576 -120
  60. data/lib/clacky/web/settings.js +213 -51
  61. data/lib/clacky/web/skills.js +5 -14
  62. data/lib/clacky/web/theme.js +1 -0
  63. data/lib/clacky/web/utils.js +57 -0
  64. data/lib/clacky/web/ws-dispatcher.js +136 -0
  65. data/scripts/build/lib/gem.sh +9 -2
  66. data/scripts/build/src/install_full.sh.cc +2 -0
  67. data/scripts/build/src/uninstall.sh.cc +1 -1
  68. data/scripts/install.ps1 +19 -5
  69. data/scripts/install.sh +9 -2
  70. data/scripts/install_full.sh +11 -2
  71. data/scripts/install_rails_deps.sh +9 -2
  72. data/scripts/uninstall.sh +10 -3
  73. metadata +9 -2
@@ -256,8 +256,7 @@ body {
256
256
  letter-spacing: 0;
257
257
  }
258
258
  .header-logo-img {
259
- height: 1.375rem;
260
- max-width: 6.875rem;
259
+ height: 2.5rem;
261
260
  object-fit: contain;
262
261
  display: block;
263
262
  flex-shrink: 0;
@@ -857,6 +856,43 @@ body {
857
856
  opacity: 0.5;
858
857
  }
859
858
 
859
+ .session-search-group {
860
+ padding: 0.5rem 0.75rem 0.25rem;
861
+ font-size: 0.6875rem;
862
+ font-weight: 600;
863
+ color: var(--color-text-muted);
864
+ text-transform: none;
865
+ letter-spacing: 0.02em;
866
+ }
867
+ .session-search-group + .session-search-group {
868
+ margin-top: 0.25rem;
869
+ }
870
+
871
+ .session-snippet {
872
+ white-space: nowrap;
873
+ overflow: hidden;
874
+ text-overflow: ellipsis;
875
+ font-size: 0.6875rem;
876
+ color: var(--color-text-muted);
877
+ margin-top: 2px;
878
+ line-height: 1.35;
879
+ opacity: 0.85;
880
+ }
881
+ .session-snippet mark {
882
+ background: var(--color-warning-bg, #fef3c7);
883
+ color: var(--color-text-primary);
884
+ padding: 0 2px;
885
+ border-radius: 2px;
886
+ font-weight: 600;
887
+ }
888
+
889
+ .session-name__text mark {
890
+ background: var(--color-warning-bg, #fef3c7);
891
+ color: var(--color-text-primary);
892
+ padding: 0 1px;
893
+ border-radius: 2px;
894
+ }
895
+
860
896
  /* Legacy .btn-new-inline — kept for compat, can be removed after cleanup */
861
897
  .btn-new-inline {
862
898
  display: none;
@@ -985,9 +1021,22 @@ body {
985
1021
  z-index: 1000;
986
1022
  animation: fadeIn 0.15s ease;
987
1023
  }
1024
+ .session-context-menu {
1025
+ background: var(--color-bg-secondary);
1026
+ border: 1px solid var(--color-border-primary);
1027
+ border-radius: 8px;
1028
+ box-shadow: 0 6px 20px rgba(0, 0, 0, 0.08), 0 2px 6px rgba(0, 0, 0, 0.04);
1029
+ padding: 0.25rem;
1030
+ min-width: 10rem;
1031
+ z-index: 1001;
1032
+ animation: fadeIn 0.15s ease;
1033
+ }
988
1034
  [data-theme="dark"] .session-actions-menu {
989
1035
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5), 0 2px 6px rgba(0, 0, 0, 0.3);
990
1036
  }
1037
+ [data-theme="dark"] .session-context-menu {
1038
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5), 0 2px 6px rgba(0, 0, 0, 0.3);
1039
+ }
991
1040
  .session-actions-menu-item {
992
1041
  display: flex;
993
1042
  align-items: center;
@@ -1623,7 +1672,7 @@ body {
1623
1672
  .sib-status-* classes instead. Legacy .status-* styles removed. */
1624
1673
 
1625
1674
  /* ── Messages ────────────────────────────────────────────────────────────── */
1626
- #messages {
1675
+ .chat-messages-scroll {
1627
1676
  flex: 1;
1628
1677
  overflow-y: auto;
1629
1678
  overflow-x: hidden;
@@ -2109,6 +2158,15 @@ body {
2109
2158
  .tu-delta-ok { color: #34d399; } /* green — normal */
2110
2159
  .tu-delta-mid { color: #f59e0b; } /* yellow — moderate */
2111
2160
  .tu-delta-high { color: #f87171; } /* red — large */
2161
+
2162
+ .tool-item .token-usage-line.tu-attached {
2163
+ align-self: stretch;
2164
+ margin: 0.25rem 0 0;
2165
+ padding: 2px 0 0;
2166
+ opacity: 0.55;
2167
+ flex-wrap: wrap;
2168
+ }
2169
+ .tool-item .token-usage-line.tu-attached:hover { opacity: 0.95; }
2112
2170
  .tu-delta-neg { color: var(--color-accent-primary); } /* cyan — compression */
2113
2171
 
2114
2172
  /* Detail fields: hidden by default, revealed on hover */
@@ -2172,7 +2230,26 @@ body {
2172
2230
  gap: 0.375rem;
2173
2231
  }
2174
2232
  .tool-item-name { color: var(--color-warning); font-weight: 600; }
2175
- .tool-item-arg { color: var(--color-text-secondary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 25rem; }
2233
+ .tool-item-arg { color: var(--color-text-secondary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; flex: 1 1 auto; }
2234
+ .tool-item-header.tool-item-expandable { cursor: pointer; user-select: none; }
2235
+ .tool-item-header.tool-item-expandable:hover .tool-item-name,
2236
+ .tool-item-header.tool-item-expandable:hover .tool-item-arg { color: var(--color-text-primary); }
2237
+ .tool-item.expanded .tool-item-arg { white-space: normal; overflow: visible; text-overflow: clip; word-break: break-all; }
2238
+ .tool-item-details {
2239
+ margin: 0.25rem 0 2px 0;
2240
+ padding: 0.5rem;
2241
+ background: var(--color-bg-secondary);
2242
+ border: 1px solid var(--color-border-secondary);
2243
+ border-radius: 4px;
2244
+ font-size: 0.6875rem;
2245
+ font-family: monospace;
2246
+ color: var(--color-text-primary);
2247
+ white-space: pre-wrap;
2248
+ word-break: break-all;
2249
+ line-height: 1.5;
2250
+ max-height: 18rem;
2251
+ overflow-y: auto;
2252
+ }
2176
2253
  .tool-item-status { margin-left: auto; font-size: 0.6875rem; flex-shrink: 0; }
2177
2254
  .tool-item-status.ok { color: var(--color-success); }
2178
2255
  .tool-item-status.err { color: var(--color-error); }
@@ -2203,6 +2280,30 @@ body {
2203
2280
  .tool-item-stdout .ansi-black { color: #5c6370; }
2204
2281
  .tool-item-stdout .ansi-bold { font-weight: 700; }
2205
2282
 
2283
+ /* ── Diff block (rendered inline within edit/write tool-item) ─────────────── */
2284
+ .tool-item-diff {
2285
+ margin: 0.25rem 0 0.25rem 1.25rem;
2286
+ padding: 0.375rem 0.5rem;
2287
+ background: var(--color-bg-secondary);
2288
+ border: 1px solid var(--color-border-secondary);
2289
+ border-radius: 4px;
2290
+ font-size: 0.6875rem;
2291
+ font-family: monospace;
2292
+ line-height: 1.5;
2293
+ max-height: 20rem;
2294
+ overflow: auto;
2295
+ }
2296
+ .diff-line {
2297
+ white-space: pre;
2298
+ padding: 0 0.25rem;
2299
+ border-radius: 2px;
2300
+ }
2301
+ .diff-line.diff-add { background: rgba(46, 160, 67, 0.18); color: #98c379; }
2302
+ .diff-line.diff-del { background: rgba(248, 81, 73, 0.18); color: #e06c75; }
2303
+ .diff-line.diff-hunk { color: var(--color-text-tertiary); font-style: italic; }
2304
+ .diff-line.diff-ctx { color: var(--color-text-secondary); }
2305
+ .diff-line.diff-more { color: var(--color-text-tertiary); font-style: italic; margin-top: 0.25rem; }
2306
+
2206
2307
  /* ── Thinking block (collapsible <think>...</think> sections) ─────────────── */
2207
2308
  .thinking-block {
2208
2309
  margin: 0 0 0.375rem 0;
@@ -9948,6 +10049,7 @@ body.setup-mode[data-theme="dark"] {
9948
10049
  background: var(--color-bg-secondary);
9949
10050
  margin-bottom: 0.5rem;
9950
10051
  overflow: hidden;
10052
+ max-width: 48rem;
9951
10053
  }
9952
10054
  .media-row.is-expanded {
9953
10055
  background: var(--color-bg-secondary);
@@ -9970,25 +10072,28 @@ body.setup-mode[data-theme="dark"] {
9970
10072
  background: var(--color-bg-primary);
9971
10073
  border: 1px solid var(--color-border-primary);
9972
10074
  border-radius: 6px;
9973
- padding: 1px;
10075
+ padding: 2px;
9974
10076
  }
9975
10077
  .media-row-segmented button {
9976
10078
  background: transparent;
9977
10079
  border: none;
9978
- padding: 0.25rem 0.625rem;
10080
+ padding: 0.3125rem 0.875rem;
9979
10081
  font-size: 0.75rem;
10082
+ font-weight: 500;
9980
10083
  color: var(--color-text-secondary);
9981
10084
  border-radius: 5px;
9982
10085
  cursor: pointer;
9983
- transition: background-color 0.15s ease, color 0.15s ease;
10086
+ transition: background-color 0.15s ease, color 0.15s ease, box-shadow 0.15s ease;
9984
10087
  font-family: inherit;
9985
10088
  }
9986
10089
  .media-row-segmented button:hover:not(.is-active):not(:disabled) {
9987
10090
  color: var(--color-text-primary);
10091
+ background: var(--color-bg-secondary);
9988
10092
  }
9989
10093
  .media-row-segmented button.is-active {
9990
10094
  background: var(--color-button-primary);
9991
10095
  color: #fff;
10096
+ box-shadow: 0 1px 2px rgba(0,0,0,0.1);
9992
10097
  }
9993
10098
  .media-row-segmented button:disabled {
9994
10099
  opacity: 0.4;
@@ -10090,3 +10195,121 @@ body.setup-mode[data-theme="dark"] {
10090
10195
  justify-content: flex-end;
10091
10196
  margin-top: 0.25rem;
10092
10197
  }
10198
+
10199
+ .media-auto-grid {
10200
+ display: grid;
10201
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
10202
+ gap: 0.625rem 1rem;
10203
+ }
10204
+ .media-provider-line {
10205
+ display: flex;
10206
+ align-items: baseline;
10207
+ gap: 0.5rem;
10208
+ font-size: 0.8125rem;
10209
+ margin-bottom: 0.625rem;
10210
+ }
10211
+ .media-provider-label {
10212
+ color: var(--color-text-secondary);
10213
+ }
10214
+ .media-provider-value {
10215
+ color: var(--color-text-primary);
10216
+ font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
10217
+ }
10218
+ .media-auto-readonly {
10219
+ background: var(--color-bg-primary);
10220
+ border: 1px solid var(--color-border-primary);
10221
+ border-radius: 6px;
10222
+ padding: 0.4375rem 0.625rem;
10223
+ font-size: 0.8125rem;
10224
+ color: var(--color-text-primary);
10225
+ font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
10226
+ word-break: break-all;
10227
+ min-height: 1.125rem;
10228
+ line-height: 1.4;
10229
+ }
10230
+ .media-custom-fields {
10231
+ display: grid;
10232
+ grid-template-columns: 1fr;
10233
+ gap: 0.625rem;
10234
+ }
10235
+ .media-custom-readonly-list {
10236
+ display: flex;
10237
+ flex-direction: column;
10238
+ gap: 0.375rem;
10239
+ }
10240
+ .media-custom-readonly-row {
10241
+ display: flex;
10242
+ align-items: baseline;
10243
+ gap: 0.5rem;
10244
+ font-size: 0.8125rem;
10245
+ line-height: 1.4;
10246
+ }
10247
+ .media-custom-readonly-label {
10248
+ flex: 0 0 auto;
10249
+ min-width: 4.5rem;
10250
+ color: var(--color-text-secondary);
10251
+ }
10252
+ .media-custom-readonly-value {
10253
+ flex: 1 1 auto;
10254
+ color: var(--color-text-primary);
10255
+ font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
10256
+ word-break: break-all;
10257
+ }
10258
+ .media-row-btn-sm {
10259
+ font-size: 0.75rem;
10260
+ padding: 0.3125rem 0.875rem;
10261
+ }
10262
+ .media-row-detail .model-test-result {
10263
+ margin-top: 0.5rem;
10264
+ min-height: 1rem;
10265
+ }
10266
+ .media-row-detail .media-row-actions {
10267
+ margin-top: 0.625rem;
10268
+ }
10269
+
10270
+ /* ── Phase grouping (folded subagent runs like skill evolution) ────────── */
10271
+ .msg-phase {
10272
+ align-self: stretch;
10273
+ margin: 0.25rem 0;
10274
+ border: 1px dashed var(--color-border-primary);
10275
+ border-radius: 8px;
10276
+ background: var(--color-bg-secondary);
10277
+ font-size: 0.8125rem;
10278
+ }
10279
+ .msg-phase-summary {
10280
+ cursor: pointer;
10281
+ padding: 0.375rem 0.625rem;
10282
+ color: var(--color-text-secondary);
10283
+ list-style: none;
10284
+ user-select: none;
10285
+ display: flex;
10286
+ align-items: center;
10287
+ gap: 0.375rem;
10288
+ }
10289
+ .msg-phase-summary::-webkit-details-marker { display: none; }
10290
+ .msg-phase-summary::before {
10291
+ content: "\25B8";
10292
+ font-size: 0.6875rem;
10293
+ transition: transform 0.15s;
10294
+ }
10295
+ .msg-phase[open] .msg-phase-summary::before { transform: rotate(90deg); }
10296
+ .msg-phase-icon { font-size: 0.875rem; }
10297
+ .msg-phase-label { font-weight: 500; }
10298
+ .msg-phase-status { opacity: 0.7; font-size: 0.75rem; }
10299
+ .msg-phase-status-incomplete { color: var(--color-warning, #c97f00); }
10300
+ .msg-phase-body {
10301
+ padding: 0.625rem 0.75rem 0.75rem;
10302
+ border-top: 1px dashed var(--color-border-primary);
10303
+ display: flex;
10304
+ flex-direction: column;
10305
+ gap: 0.625rem;
10306
+ }
10307
+ .msg-phase-body .msg { opacity: 0.9; }
10308
+
10309
+ .msg-phase-empty {
10310
+ opacity: 0.55;
10311
+ border-style: dotted;
10312
+ }
10313
+ .msg-phase-empty .msg-phase-summary { cursor: default; }
10314
+ .msg-phase-empty .msg-phase-summary::before { visibility: hidden; }
10315
+ .msg-phase-empty .msg-phase-body { display: none; }
@@ -335,15 +335,15 @@ const Modal = (() => {
335
335
  $("prompt-modal-ok").onclick = null;
336
336
  $("prompt-modal-cancel").onclick = null;
337
337
  input.onkeydown = null;
338
+ unbindEnter();
338
339
  resolve(result);
339
340
  };
340
341
 
341
342
  $("prompt-modal-ok").onclick = () => cleanup(input.value.trim() || null);
342
343
  $("prompt-modal-cancel").onclick = () => cleanup(null);
343
-
344
- // Support Enter to confirm, Escape to cancel
344
+
345
+ const unbindEnter = IME.bindEnter(input, () => cleanup(input.value.trim() || null));
345
346
  input.onkeydown = (e) => {
346
- if (e.key === "Enter") cleanup(input.value.trim() || null);
347
347
  if (e.key === "Escape") cleanup(null);
348
348
  };
349
349
  });
@@ -369,6 +369,7 @@ const Modal = (() => {
369
369
  $("rename-modal-overlay").onclick = null;
370
370
  input.onkeydown = null;
371
371
  input.oninput = null;
372
+ unbindEnter();
372
373
  resolve(result);
373
374
  };
374
375
 
@@ -386,9 +387,9 @@ const Modal = (() => {
386
387
 
387
388
  $("rename-modal-save").onclick = saveHandler;
388
389
  $("rename-modal-cancel").onclick = () => cleanup(null);
389
-
390
+
391
+ const unbindEnter = IME.bindEnter(input, saveHandler);
390
392
  input.onkeydown = (e) => {
391
- if (e.key === "Enter") { e.preventDefault(); saveHandler(); }
392
393
  if (e.key === "Escape") cleanup(null);
393
394
  };
394
395
 
@@ -329,13 +329,16 @@ const Brand = (() => {
329
329
  };
330
330
  img.src = info.logo_url;
331
331
  }
332
- } else {
333
- // No logo configured — hide logo image and remove has-logo class
332
+ } else if (info.product_name) {
333
+ // Brand configured but no logo — hide the image, brand name text is enough
334
334
  if (logoImg) {
335
335
  logoImg.style.display = "none";
336
336
  logoImg.src = "";
337
337
  }
338
338
  if (brandWrap) brandWrap.classList.remove("has-logo");
339
+ } else {
340
+ // No brand at all — show default OpenClacky logo
341
+ _applyDefaultLogo();
339
342
  }
340
343
 
341
344
  // Always show brand name text; hide it only when no brand name is set
@@ -355,6 +358,18 @@ const Brand = (() => {
355
358
  });
356
359
  }
357
360
 
361
+ // Apply the default OpenClacky logo based on current theme.
362
+ function _applyDefaultLogo() {
363
+ const logoImg = document.getElementById("header-logo-img");
364
+ const brandWrap = document.getElementById("header-brand");
365
+ if (!logoImg) return;
366
+
367
+ logoImg.src = "/logo_nav_dark.png";
368
+ logoImg.alt = "OpenClacky";
369
+ logoImg.style.display = "";
370
+ if (brandWrap) brandWrap.classList.add("has-logo");
371
+ }
372
+
358
373
  // Replace the browser tab favicon with the given URL.
359
374
  // Works for both image URLs and SVG data URIs.
360
375
  function _applyFavicon(url) {
@@ -510,5 +525,10 @@ const Brand = (() => {
510
525
  }
511
526
  }
512
527
 
528
+ // Both themes use the same transparent A版 logo via CSS, no swap needed.
529
+ window.addEventListener("clacky-theme-change", e => {
530
+ // No-op
531
+ });
532
+
513
533
  return { check, refresh, applyBrandName: _applyBrandName, applyHeaderLogo: _applyHeaderLogo, applyOwnerBadge: _applyOwnerBadge, applyGetSerialLink: _applyGetSerialLink, clearBrandCache: _clearBrandCache, goToLicenseInput: _goToLicenseInput, get userLicensed() { return _userLicensed; }, get branded() { return _branded; } };
514
534
  })();
Binary file
@@ -69,6 +69,9 @@ const I18n = (() => {
69
69
  // ── Session list ──
70
70
  "sessions.empty": "No sessions yet",
71
71
  "sessions.confirmDelete": "Delete session {{name}}? This cannot be undone.",
72
+ "sessions.search.byName": "Title matches ({{n}})",
73
+ "sessions.search.byContent": "Content matches ({{n}})",
74
+ "sessions.search.contentEmpty": "No content matches",
72
75
  "sessions.meta": "{{tasks}} tasks · ${{cost}}",
73
76
  "sessions.metaTasks": "{{n}} tasks",
74
77
  "sessions.deleteTitle": "Delete session",
@@ -99,6 +102,8 @@ const I18n = (() => {
99
102
  "workspace.downloadFailed": "Download failed",
100
103
  "sib.model.tooltip": "Click to switch model",
101
104
  "sib.model.tooltip.busy": "Model switching is disabled while the agent is responding",
105
+ "sib.variant.header": "Quick switch",
106
+ "sib.variant.default": "default",
102
107
  "sib.signal.tooltip": "Recent LLM latency",
103
108
  "sib.reasoning.tooltip": "Click to change reasoning effort",
104
109
  "sib.reasoning.label": "Reasoning",
@@ -120,6 +125,7 @@ const I18n = (() => {
120
125
  "sessions.newSession": "+ New Session",
121
126
  "sessions.actions.pin": "Pin",
122
127
  "sessions.actions.unpin": "Unpin",
128
+ "sessions.actions.fork": "Fork",
123
129
  "sessions.actions.rename": "Rename",
124
130
  "sessions.actions.delete": "Delete",
125
131
  "sessions.newSessionAdvanced": "More Options",
@@ -513,16 +519,19 @@ const I18n = (() => {
513
519
  "settings.media.field.apiKey": "API Key",
514
520
  "settings.media.field.provider":"Provider",
515
521
  "settings.media.off.hint": "Disabled.",
516
- "settings.media.auto.followsDefault": "Follows default chat model",
522
+ "settings.media.auto.followsDefault": "Auto-enabled via your default provider",
523
+ "settings.media.auto.stale": "Selected model {{requested}} is unavailable on the current provider — using {{current}} instead.",
517
524
  "settings.media.auto.noDefaultModel": "Set a default chat model first.",
518
525
  "settings.media.auto.unsupported": "Current provider has no built-in model for this kind. Switch to Custom.",
519
526
  "settings.media.auto.comingSoon": "Not available yet — no built-in providers.",
520
- "settings.media.auto.disabledTitle": "No built-in provider available — use Custom.",
527
+ "settings.media.auto.disabledTitle": "Provider has no built-in model — use Custom.",
521
528
  "settings.media.action.edit": "Edit",
522
529
  "settings.media.action.save": "Save",
523
530
  "settings.media.action.cancel": "Cancel",
524
531
  "settings.media.action.saving": "Saving…",
525
532
  "settings.media.action.saved": "Saved",
533
+ "settings.media.action.test": "Test",
534
+ "settings.media.testing": "Testing connection…",
526
535
  "settings.media.apiKey.placeholder": "Enter API key",
527
536
  "settings.media.apiKey.required": "API key required",
528
537
  "settings.media.model.required": "Model name required",
@@ -801,6 +810,9 @@ const I18n = (() => {
801
810
  // ── Session list ──
802
811
  "sessions.empty": "暂无会话",
803
812
  "sessions.confirmDelete": "删除会话 {{name}}?此操作不可撤销。",
813
+ "sessions.search.byName": "标题匹配 ({{n}})",
814
+ "sessions.search.byContent": "内容匹配 ({{n}})",
815
+ "sessions.search.contentEmpty": "没有内容匹配",
804
816
  "sessions.meta": "{{tasks}} 个任务 · ${{cost}}",
805
817
  "sessions.metaTasks": "{{n}} 个任务",
806
818
  "sessions.deleteTitle": "删除会话",
@@ -831,6 +843,8 @@ const I18n = (() => {
831
843
  "workspace.downloadFailed": "下载失败",
832
844
  "sib.model.tooltip": "点击切换模型",
833
845
  "sib.model.tooltip.busy": "Agent 回复中,暂时无法切换模型",
846
+ "sib.variant.header": "快速切换",
847
+ "sib.variant.default": "默认",
834
848
  "sib.signal.tooltip": "最近一次 LLM 响应延迟",
835
849
  "sib.reasoning.tooltip": "点击调整思考等级",
836
850
  "sib.reasoning.label": "思考",
@@ -854,6 +868,7 @@ const I18n = (() => {
854
868
  "sessions.loadMore": "加载更多会话",
855
869
  "sessions.actions.pin": "置顶",
856
870
  "sessions.actions.unpin": "取消置顶",
871
+ "sessions.actions.fork": "复制会话",
857
872
  "sessions.actions.rename": "重命名",
858
873
  "sessions.actions.delete": "删除",
859
874
  "sessions.loadingMore": "加载中…",
@@ -1244,16 +1259,19 @@ const I18n = (() => {
1244
1259
  "settings.media.field.apiKey": "API Key",
1245
1260
  "settings.media.field.provider":"服务商",
1246
1261
  "settings.media.off.hint": "已关闭。",
1247
- "settings.media.auto.followsDefault": "跟随默认聊天模型",
1262
+ "settings.media.auto.followsDefault": "该服务商已支持,已自动启用",
1263
+ "settings.media.auto.stale": "你选定的 {{requested}} 在当前服务商不可用,已临时使用 {{current}}。",
1248
1264
  "settings.media.auto.noDefaultModel": "请先设置默认聊天模型。",
1249
1265
  "settings.media.auto.unsupported": "当前服务商无内置模型,请切换到「自定义」。",
1250
1266
  "settings.media.auto.comingSoon": "暂无内置服务商,敬请期待。",
1251
- "settings.media.auto.disabledTitle": "暂无内置服务商,请使用自定义。",
1267
+ "settings.media.auto.disabledTitle": "服务商尚未配置支持,请使用自定义",
1252
1268
  "settings.media.action.edit": "编辑",
1253
1269
  "settings.media.action.save": "保存",
1254
1270
  "settings.media.action.cancel": "取消",
1255
1271
  "settings.media.action.saving": "保存中…",
1256
1272
  "settings.media.action.saved": "已保存",
1273
+ "settings.media.action.test": "测试",
1274
+ "settings.media.testing": "测试连接中…",
1257
1275
  "settings.media.apiKey.placeholder": "请输入 API Key",
1258
1276
  "settings.media.apiKey.required": "请填写 API Key",
1259
1277
  "settings.media.model.required": "请填写模型名称",
@@ -4,7 +4,8 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title id="page-title">{{BRAND_NAME}}</title>
7
- <link rel="icon" type="image/svg+xml" href="/icon.svg">
7
+ <link rel="icon" href="/favicon.ico">
8
+ <link rel="apple-touch-icon" href="/apple-touch-icon-180.png">
8
9
  <link rel="stylesheet" href="/vendor/katex/katex.min.css">
9
10
  <link rel="stylesheet" href="/vendor/hljs/hljs-theme.css">
10
11
  <link rel="stylesheet" href="/app.css">
@@ -30,8 +31,8 @@
30
31
  <path d="M9 3v18"/>
31
32
  </svg>
32
33
  </button>
33
- <div id="header-brand" style="cursor:pointer" onclick="Router.navigate('chat')">
34
- <img class="header-logo-img" id="header-logo-img" src="" alt="" style="display:none">
34
+ <div id="header-brand" class="has-logo" style="cursor:pointer" onclick="Router.navigate('chat')">
35
+ <img class="header-logo-img" id="header-logo-img" src="/logo_nav_dark.png" alt="OpenClacky">
35
36
  <span class="header-logo-divider"></span>
36
37
  <span class="header-logo" id="header-logo">{{BRAND_NAME}}</span>
37
38
  <!-- Owner badge: visible only for creator-tier licenses (user_licensed=true).
@@ -315,7 +316,7 @@
315
316
 
316
317
  <!-- Chat column (messages + info bar + input) -->
317
318
  <div id="chat-main">
318
- <div id="messages"></div>
319
+ <div id="messages" class="chat-messages-scroll"></div>
319
320
  <!-- New message notification banner -->
320
321
  <div id="new-message-banner" class="new-message-banner" style="display:none">
321
322
  <span data-i18n="chat.newMessageHint">New messages ↓</span>
@@ -1295,6 +1296,7 @@
1295
1296
  <script src="/vendor/hljs/highlight.min.js"></script>
1296
1297
  <script src="/vendor/katex/katex.min.js"></script>
1297
1298
  <script src="/vendor/katex/auto-render.min.js"></script>
1299
+ <script src="/utils.js"></script>
1298
1300
  <script src="/i18n.js"></script>
1299
1301
  <script src="/auth.js"></script>
1300
1302
  <script src="/theme.js"></script>
Binary file
@@ -23,7 +23,14 @@ window.ModelTester = (function () {
23
23
  return { ok: false, message: e.message };
24
24
  }
25
25
 
26
- if (!data.ok) return { ok: false, message: data.message || "" };
26
+ if (!data.ok) {
27
+ const msg = data.message || "";
28
+ const code = data.error_code || "";
29
+ if (code === "insufficient_credit") {
30
+ return { ok: false, message: I18n.t("error.insufficient_credit"), error_code: code };
31
+ }
32
+ return { ok: false, message: msg, error_code: code };
33
+ }
27
34
 
28
35
  if (data.effective_base_url && data.effective_base_url !== base_url) {
29
36
  return { ok: true, base_url: data.effective_base_url, message: data.message || "", rewrote: true };