openclacky 1.3.11 → 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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +59 -0
  3. data/lib/clacky/agent/session_serializer.rb +11 -4
  4. data/lib/clacky/agent/skill_manager.rb +1 -0
  5. data/lib/clacky/agent.rb +4 -5
  6. data/lib/clacky/brand_config.rb +53 -3
  7. data/lib/clacky/default_extensions/ext-studio/api/handler.rb +202 -3
  8. data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +610 -60
  9. data/lib/clacky/default_skills/media-gen/SKILL.md +182 -7
  10. data/lib/clacky/default_skills/media-gen/scripts/video_seq.sh +5 -3
  11. data/lib/clacky/extension/packager.rb +21 -1
  12. data/lib/clacky/extension/verifier.rb +1 -1
  13. data/lib/clacky/media/generator.rb +41 -0
  14. data/lib/clacky/media/volcengine.rb +394 -0
  15. data/lib/clacky/platform_http_client.rb +9 -7
  16. data/lib/clacky/providers.rb +4 -4
  17. data/lib/clacky/server/http_server.rb +67 -22
  18. data/lib/clacky/tools/grep.rb +6 -1
  19. data/lib/clacky/tools/terminal.rb +24 -8
  20. data/lib/clacky/ui2/layout_manager.rb +4 -0
  21. data/lib/clacky/ui2/screen_buffer.rb +31 -3
  22. data/lib/clacky/utils/environment_detector.rb +16 -9
  23. data/lib/clacky/utils/file_processor.rb +30 -28
  24. data/lib/clacky/utils/model_pricing.rb +49 -0
  25. data/lib/clacky/version.rb +1 -1
  26. data/lib/clacky/web/app.css +229 -31
  27. data/lib/clacky/web/components/onboard.js +14 -4
  28. data/lib/clacky/web/features/backup/view.js +4 -3
  29. data/lib/clacky/web/features/extensions/store.js +2 -2
  30. data/lib/clacky/web/features/extensions/view.js +37 -7
  31. data/lib/clacky/web/features/mcp/view.js +5 -2
  32. data/lib/clacky/web/features/skills/view.js +61 -1
  33. data/lib/clacky/web/features/workspace/store.js +11 -0
  34. data/lib/clacky/web/features/workspace/view.js +10 -4
  35. data/lib/clacky/web/i18n.js +14 -2
  36. data/lib/clacky/web/index.html +29 -13
  37. data/lib/clacky/web/sessions.js +45 -8
  38. data/scripts/uninstall.sh +1 -1
  39. metadata +2 -1
@@ -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.625rem;
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
@@ -782,7 +859,7 @@ body {
782
859
  color: var(--color-accent-primary);
783
860
  }
784
861
 
785
- /* ── Command-palette search overlay (⌘K) ─────────────────────────────────── */
862
+ /* ── Command-palette search overlay ──────────────────────────────────────── */
786
863
  .cmd-palette-overlay {
787
864
  position: fixed;
788
865
  inset: 0;
@@ -6484,6 +6561,28 @@ body {
6484
6561
  overflow-y: auto;
6485
6562
  }
6486
6563
 
6564
+ /* ── Skills search ──────────────────────────────────────────────────────── */
6565
+ .skills-search {
6566
+ font-size: 0.8125rem;
6567
+ padding: 0.375rem 0.75rem;
6568
+ border-radius: 6px;
6569
+ border: 1px solid var(--color-border-primary);
6570
+ background: var(--color-bg-secondary);
6571
+ color: var(--color-text-primary);
6572
+ width: 12rem;
6573
+ }
6574
+ .skills-search::placeholder { color: var(--color-text-tertiary); }
6575
+ .skills-search:focus {
6576
+ outline: none;
6577
+ border-color: var(--color-accent, #6366f1);
6578
+ }
6579
+ .skills-search-empty {
6580
+ padding: 2rem 1rem;
6581
+ text-align: center;
6582
+ color: var(--color-text-tertiary);
6583
+ font-size: 0.875rem;
6584
+ }
6585
+
6487
6586
  /* ── My Skills upload area (user-licensed only) ───────────────────────────── */
6488
6587
  .my-skills-upload-area {
6489
6588
  display: flex;
@@ -7171,7 +7270,7 @@ body {
7171
7270
  background: var(--color-bg-secondary);
7172
7271
  border: 1px solid var(--color-border-primary);
7173
7272
  border-radius: 8px;
7174
- padding: 0.875rem 1rem;
7273
+ padding: 0.6rem 1rem;
7175
7274
  transition: border-color .15s;
7176
7275
  }
7177
7276
  .extension-card:hover { border-color: var(--color-text-muted); }
@@ -7184,13 +7283,25 @@ body {
7184
7283
  font-size: 1.5rem;
7185
7284
  line-height: 1.4;
7186
7285
  flex-shrink: 0;
7286
+ display: flex;
7287
+ align-items: flex-start;
7288
+ }
7289
+ .extension-default-icon {
7290
+ width: 2rem;
7291
+ height: 100%;
7292
+ display: block;
7293
+ border-radius: 7px;
7294
+ }
7295
+ .extension-emoji-lg .extension-default-icon {
7296
+ width: 2.75rem;
7297
+ height: 2.75rem;
7298
+ border-radius: 10px;
7187
7299
  }
7188
- .extension-info { flex: 1; min-width: 0; }
7300
+ .extension-info { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 4px; }
7189
7301
  .extension-title {
7190
7302
  display: flex;
7191
7303
  align-items: center;
7192
7304
  gap: 0.5rem;
7193
- margin-bottom: 0.25rem;
7194
7305
  flex-wrap: wrap;
7195
7306
  }
7196
7307
  .extension-name {
@@ -7203,7 +7314,6 @@ body {
7203
7314
  color: var(--color-text-secondary);
7204
7315
  background: var(--color-bg-tertiary, rgba(255,255,255,0.05));
7205
7316
  border-radius: 4px;
7206
- padding: 0.0625rem 0.375rem;
7207
7317
  }
7208
7318
  .extension-units {
7209
7319
  font-size: 0.6875rem;
@@ -7234,11 +7344,10 @@ body {
7234
7344
  font-size: 0.75rem;
7235
7345
  color: var(--color-text-secondary);
7236
7346
  line-height: 1.5;
7237
- margin-bottom: 0.375rem;
7238
7347
  }
7239
7348
  .extension-desc-detail {
7240
7349
  font-size: 0.8125rem;
7241
- margin-bottom: 0.5rem;
7350
+ margin-bottom: 0;
7242
7351
  }
7243
7352
  .extension-detail-author {
7244
7353
  font-size: 0.6875rem;
@@ -7300,8 +7409,13 @@ body {
7300
7409
  padding-bottom: 1.25rem;
7301
7410
  border-bottom: 1px solid var(--color-border-primary);
7302
7411
  }
7303
- .extension-detail-heading { flex: 1; min-width: 0; }
7412
+ .extension-detail-heading { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 6px; }
7304
7413
  .extension-emoji-lg { font-size: 2.5rem; line-height: 1; margin-top: 0.125rem; }
7414
+ .extension-emoji-lg .extension-default-icon {
7415
+ width: 3.5rem;
7416
+ height: 3.5rem;
7417
+ border-radius: 12px;
7418
+ }
7305
7419
  .extension-name-lg { font-size: 1.125rem; font-weight: 700; }
7306
7420
  .extension-detail-block { margin-top: 1.5rem; }
7307
7421
  .extension-detail-block-title {
@@ -7309,9 +7423,40 @@ body {
7309
7423
  font-weight: 600;
7310
7424
  text-transform: uppercase;
7311
7425
  letter-spacing: 0.04em;
7312
- color: var(--color-text-muted);
7313
7426
  margin: 0 0 0.75rem;
7314
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; }
7315
7460
  .extension-detail-section { margin-bottom: 0.875rem; }
7316
7461
  .extension-detail-section-title {
7317
7462
  font-size: 0.6875rem;
@@ -7371,13 +7516,17 @@ body {
7371
7516
  border-bottom: 1px solid var(--color-border-primary);
7372
7517
  }
7373
7518
  .extension-version-row .extension-version {
7374
- font-size: 0.9375rem;
7519
+ font-size: 0.8375rem;
7375
7520
  font-weight: 700;
7376
7521
  color: var(--color-text-primary);
7377
7522
  background: none;
7378
7523
  padding: 0;
7379
7524
  border-radius: 0;
7380
7525
  }
7526
+ .extension-version-separator {
7527
+ font-size: 0.8125rem;
7528
+ color: var(--color-text-secondary);
7529
+ }
7381
7530
  .extension-version-date {
7382
7531
  font-size: 0.6875rem;
7383
7532
  color: var(--color-text-muted);
@@ -7423,7 +7572,7 @@ body {
7423
7572
  display: flex;
7424
7573
  align-items: center;
7425
7574
  gap: 0.5rem;
7426
- margin-top: 0.75rem;
7575
+ margin-top: 0.25rem;
7427
7576
  }
7428
7577
  .extension-disabled {
7429
7578
  font-size: 0.6875rem;
@@ -7631,7 +7780,7 @@ body {
7631
7780
  color: #ffffff;
7632
7781
  border: 1px solid var(--color-accent-primary);
7633
7782
  }
7634
- .btn-brand-update:hover { background: var(--color-accent-hover); }
7783
+ .btn-brand-update:hover { background: var(--color-button-primary-hover); }
7635
7784
  .btn-brand-install:disabled,
7636
7785
  .btn-brand-update:disabled {
7637
7786
  opacity: 0.5;
@@ -10055,7 +10204,7 @@ body.setup-mode[data-theme="dark"] {
10055
10204
 
10056
10205
  .memory-card-actions {
10057
10206
  display: flex;
10058
- align-items: flex-start;
10207
+ align-items: center;
10059
10208
  gap: 0.375rem;
10060
10209
  flex-shrink: 0;
10061
10210
  }
@@ -10095,6 +10244,7 @@ body.setup-mode[data-theme="dark"] {
10095
10244
 
10096
10245
  .btn-memory-expand {
10097
10246
  min-width: 1.75rem;
10247
+ align-self: stretch;
10098
10248
  justify-content: center;
10099
10249
  color: var(--color-text-muted);
10100
10250
  }
@@ -10608,7 +10758,7 @@ body.setup-mode[data-theme="dark"] {
10608
10758
 
10609
10759
  /* ── Model Filter ────────────────────────────────────────────────────── */
10610
10760
  .billing-model-filter {
10611
- padding: 0.4375rem 0.75rem;
10761
+ padding: 0.4375rem 1.75rem 0.4375rem 0.75rem;
10612
10762
  border: 1px solid var(--color-border-primary);
10613
10763
  border-radius: 8px;
10614
10764
  background: var(--color-bg-secondary);
@@ -10616,6 +10766,23 @@ body.setup-mode[data-theme="dark"] {
10616
10766
  font-size: 0.8125rem;
10617
10767
  cursor: pointer;
10618
10768
  min-width: 130px;
10769
+ appearance: none;
10770
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%23888' stroke-width='1.5' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
10771
+ background-repeat: no-repeat;
10772
+ background-position: right 0.625rem center;
10773
+ transition: border-color 0.15s ease;
10774
+ }
10775
+ .billing-model-filter:hover {
10776
+ border-color: var(--color-border-strong);
10777
+ }
10778
+ .billing-model-filter:focus {
10779
+ outline: none;
10780
+ border-color: var(--color-accent-primary);
10781
+ box-shadow: 0 0 0 3px var(--color-accent-soft);
10782
+ }
10783
+ .billing-model-filter option {
10784
+ background: var(--color-bg-secondary);
10785
+ color: var(--color-text-primary);
10619
10786
  }
10620
10787
 
10621
10788
  /* ── Action Buttons ──────────────────────────────────────────────────── */
@@ -11374,12 +11541,11 @@ body.setup-mode[data-theme="dark"] {
11374
11541
  color: var(--color-danger, #c33);
11375
11542
  }
11376
11543
  .mcp-tools-header {
11377
- font-size: 0.6875rem;
11544
+ font-size: 0.8125rem;
11378
11545
  font-weight: 600;
11379
11546
  color: var(--color-text-secondary);
11380
11547
  margin-bottom: 0.5rem;
11381
- text-transform: uppercase;
11382
- letter-spacing: 0.05em;
11548
+ letter-spacing: 0.01em;
11383
11549
  }
11384
11550
  .mcp-tool-list {
11385
11551
  list-style: none;
@@ -11391,10 +11557,9 @@ body.setup-mode[data-theme="dark"] {
11391
11557
  }
11392
11558
  .mcp-tool-item {
11393
11559
  display: flex;
11394
- align-items: baseline;
11395
- gap: 0.625rem;
11396
- font-size: 0.8125rem;
11397
- padding: 0.4375rem 0;
11560
+ flex-direction: column;
11561
+ gap: 0.3rem;
11562
+ padding: 0.625rem 0;
11398
11563
  border-bottom: 1px solid var(--color-border-primary, var(--color-border));
11399
11564
  }
11400
11565
  .mcp-tool-item:last-child {
@@ -11402,18 +11567,15 @@ body.setup-mode[data-theme="dark"] {
11402
11567
  }
11403
11568
  .mcp-tool-name {
11404
11569
  font-family: var(--font-mono);
11405
- background: var(--color-bg-elevated, var(--color-bg-secondary));
11406
- padding: 0.125rem 0.4375rem;
11407
- border-radius: 4px;
11408
- font-size: 0.75rem;
11570
+ font-size: 0.8125rem;
11571
+ font-weight: 600;
11409
11572
  color: var(--color-text-primary, var(--color-text));
11410
- flex-shrink: 0;
11411
- border: 1px solid var(--color-border-primary, var(--color-border));
11412
11573
  }
11413
11574
  .mcp-tool-desc {
11414
11575
  color: var(--color-text-secondary);
11415
- flex: 1;
11416
11576
  line-height: 1.5;
11577
+ font-size: 0.8125rem;
11578
+ opacity: 0.6;
11417
11579
  }
11418
11580
 
11419
11581
  /* ── MCP CTA banner & action buttons ─────────────────────────────────── */
@@ -12398,11 +12560,48 @@ body.setup-mode[data-theme="dark"] {
12398
12560
  display: none;
12399
12561
  }
12400
12562
 
12563
+ /* Collapse session search into the right-side header actions */
12564
+ #header-center {
12565
+ justify-content: flex-end;
12566
+ padding: 0;
12567
+ }
12568
+ #header-cmdbar {
12569
+ flex: 0 0 1.875rem;
12570
+ width: 1.875rem;
12571
+ max-width: 1.875rem;
12572
+ padding: 0;
12573
+ border: none;
12574
+ border-radius: var(--radius-sm);
12575
+ background: transparent;
12576
+ justify-content: center;
12577
+ gap: 0;
12578
+ margin-right: 0.125rem;
12579
+ }
12580
+ #header-cmdbar:hover {
12581
+ color: var(--color-text-primary);
12582
+ }
12583
+ #header-cmdbar svg {
12584
+ width: 1rem;
12585
+ height: 1rem;
12586
+ opacity: 1;
12587
+ }
12588
+ #header-cmdbar .cmdbar-ph,
12589
+ #header-cmdbar .cmdbar-kbd,
12590
+ #ext-slot-header-right:empty {
12591
+ display: none;
12592
+ }
12593
+
12401
12594
  /* Tighten right-side icon gap on mobile */
12402
12595
  #header-right {
12403
12596
  gap: 0.125rem;
12404
12597
  }
12405
12598
 
12599
+ @media (max-width: 360px) {
12600
+ #top-header {
12601
+ padding: 0 0.375rem;
12602
+ }
12603
+ }
12604
+
12406
12605
  /* Session info bar: single-line, no hover-expand, font smaller */
12407
12606
  #session-info-bar {
12408
12607
  padding: 0.1875rem 0.5rem;
@@ -12819,4 +13018,3 @@ body.setup-mode[data-theme="dark"] {
12819
13018
  background: var(--color-bg-primary);
12820
13019
  }
12821
13020
  }
12822
-
@@ -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
- $("setup-device-block").style.display = "";
115
- $("setup-manual-toggle").style.display = "";
116
- $("setup-manual-section").style.display = "none";
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 = I18n.t("settings.backup.downloaded"); statusEl.className = "model-test-result success"; }
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 = I18n.t("settings.backup.restartOk");
112
- el.className = "model-test-result success";
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
  }
@@ -22,7 +22,7 @@ const ExtensionsStore = (() => {
22
22
  let _extensions = []; // [{ id, name, name_zh, description, ..., units }]
23
23
  let _allExtensions = []; // unfiltered result from server
24
24
  let _query = ""; // current search text
25
- let _sort = "newest"; // "newest" | "updated" | "downloads"
25
+ let _sort = "downloads"; // "newest" | "updated" | "downloads"
26
26
  let _filterInstalled = false; // when true, show only installed extensions
27
27
  let _loading = false;
28
28
  let _error = null; // soft warning when the store is unreachable
@@ -102,7 +102,7 @@ const ExtensionsStore = (() => {
102
102
 
103
103
  /** Set the sort order and reload. */
104
104
  setSort(sort) {
105
- _sort = sort || "newest";
105
+ _sort = sort || "downloads";
106
106
  return Extensions.load();
107
107
  },
108
108
 
@@ -77,13 +77,27 @@ const ExtensionsView = (() => {
77
77
  return parts.join(" · ");
78
78
  }
79
79
 
80
+ function _defaultIcon(name, extraClass) {
81
+ const letter = (name || "?")[0].toUpperCase();
82
+ const colors = [
83
+ ["#6366f1","#818cf8"], ["#8b5cf6","#a78bfa"], ["#ec4899","#f472b6"],
84
+ ["#f59e0b","#fbbf24"], ["#10b981","#34d399"], ["#3b82f6","#60a5fa"],
85
+ ["#ef4444","#f87171"], ["#14b8a6","#2dd4bf"],
86
+ ];
87
+ const idx = (name || "").charCodeAt(0) % colors.length;
88
+ const [c1, c2] = colors[idx];
89
+ const gid = `eg-${idx}-${Math.random().toString(36).slice(2,7)}`;
90
+ const cls = extraClass ? `extension-emoji ${extraClass}` : "extension-emoji";
91
+ return `<span class="${cls}"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" class="extension-default-icon"><defs><linearGradient id="${gid}" x1="0" y1="0" x2="1" y2="1"><stop offset="0%" stop-color="${c1}"/><stop offset="100%" stop-color="${c2}"/></linearGradient></defs><rect width="32" height="32" rx="8" fill="url(#${gid})"/><text x="16" y="22" text-anchor="middle" font-family="system-ui,sans-serif" font-size="16" font-weight="700" fill="white">${letter}</text></svg></span>`;
92
+ }
93
+
80
94
  function _renderCard(ext) {
81
95
  const currentLang = I18n.lang();
82
- const name = (currentLang === "zh" && ext.name_zh) ? ext.name_zh : ext.name;
96
+ const name = (currentLang === "zh" && ext.display_name_zh) ? ext.display_name_zh : (ext.display_name || ext.name);
83
97
  const description = (currentLang === "zh" && ext.description_zh)
84
98
  ? ext.description_zh
85
99
  : ext.description || "";
86
- const emoji = ext.emoji || "🧩";
100
+ const emojiHtml = ext.emoji ? `<span class="extension-emoji">${escapeHtml(ext.emoji)}</span>` : _defaultIcon(ext.name);
87
101
 
88
102
  const versionHtml = ext.version
89
103
  ? `<span class="extension-version">v${escapeHtml(String(ext.version))}</span>` : "";
@@ -110,7 +124,7 @@ const ExtensionsView = (() => {
110
124
  card.dataset.extId = ext.id != null ? String(ext.id) : (ext.name || "");
111
125
  card.innerHTML = `
112
126
  <div class="extension-card-main">
113
- <div class="extension-emoji">${escapeHtml(emoji)}</div>
127
+ ${emojiHtml}
114
128
  <div class="extension-info">
115
129
  <div class="extension-title">
116
130
  <span class="extension-name">${escapeHtml(name)}</span>
@@ -229,11 +243,14 @@ const ExtensionsView = (() => {
229
243
 
230
244
  function _detailContent(ext) {
231
245
  const currentLang = I18n.lang();
232
- const name = (currentLang === "zh" && ext.name_zh) ? ext.name_zh : ext.name;
246
+ const name = (currentLang === "zh" && ext.display_name_zh) ? ext.display_name_zh : (ext.display_name || ext.name);
233
247
  const description = (currentLang === "zh" && ext.description_zh)
234
248
  ? ext.description_zh
235
249
  : ext.description || "";
236
250
  const emoji = ext.emoji || "🧩";
251
+ const detailEmojiHtml = ext.emoji
252
+ ? `<span class="extension-emoji extension-emoji-lg">${escapeHtml(ext.emoji)}</span>`
253
+ : _defaultIcon(ext.name, "extension-emoji-lg");
237
254
 
238
255
  const canUpdate = ext.installed && ext.installed_version && ext.version && ext.installed_version !== ext.version;
239
256
  const versionHtml = ext.version
@@ -258,7 +275,7 @@ const ExtensionsView = (() => {
258
275
 
259
276
  return `
260
277
  <div class="extension-detail-hero">
261
- <div class="extension-emoji extension-emoji-lg">${escapeHtml(emoji)}</div>
278
+ ${detailEmojiHtml}
262
279
  <div class="extension-detail-heading">
263
280
  <div class="extension-title">
264
281
  <span class="extension-name extension-name-lg">${escapeHtml(name)}</span>
@@ -274,10 +291,23 @@ const ExtensionsView = (() => {
274
291
  ${_renderActions(ext)}
275
292
  </div>
276
293
  </div>
277
- ${_renderContributes(ext.contributes)}
294
+ ${_renderReadme(ext.readme)}
278
295
  ${_renderVersions(ext.versions)}`;
279
296
  }
280
297
 
298
+ // Renders Markdown readme content. Uses marked.js if available, falls back to plain text.
299
+ function _renderReadme(readme) {
300
+ if (!readme || !readme.trim()) return "";
301
+ const html = typeof marked !== "undefined"
302
+ ? marked.parse(readme, { breaks: true, gfm: true })
303
+ : `<pre style="white-space:pre-wrap">${escapeHtml(readme)}</pre>`;
304
+ return `
305
+ <div class="extension-detail-block extension-readme">
306
+ <h3 class="extension-detail-block-title">${escapeHtml(I18n.t("extensions.detail.readme"))}</h3>
307
+ <div class="extension-readme-body">${html}</div>
308
+ </div>`;
309
+ }
310
+
281
311
  // Manage buttons for a locally installed extension: enable/disable toggle
282
312
  // (always available when installed) plus remove (installed layer only).
283
313
  function _renderActions(ext) {
@@ -365,7 +395,7 @@ const ExtensionsView = (() => {
365
395
  <li class="extension-version-item">
366
396
  <div class="extension-version-row">
367
397
  <span class="extension-version">v${escapeHtml(String(v.version || ""))}</span>
368
- ${date ? `<span class="extension-version-date">${escapeHtml(date)}</span>` : ""}
398
+ ${date ? `<span class="extension-version-separator">-</span><span class="extension-version-date">${escapeHtml(date)}</span>` : ""}
369
399
  </div>
370
400
  ${v.release_notes ? `<div class="extension-version-notes">${typeof marked !== "undefined" ? marked.parse(String(v.release_notes).replace(/^#{1,3}[^\n]*\n?/, ""), { breaks: true, gfm: true }) : escapeHtml(String(v.release_notes))}</div>` : ""}
371
401
  </li>`;
@@ -84,6 +84,11 @@ const McpView = (() => {
84
84
 
85
85
  data.servers.forEach(server => {
86
86
  list.appendChild(_renderCard(server));
87
+ // _renderTools uses getElementById which requires the card to already be
88
+ // in the DOM, so we call it after appendChild (not inside _renderCard).
89
+ if (McpStore.state.isExpanded(server.name) && !server.disabled) {
90
+ _renderTools(server.name);
91
+ }
87
92
  });
88
93
  }
89
94
 
@@ -170,8 +175,6 @@ const McpView = (() => {
170
175
  card.querySelector(`#toggle-mcp-${CSS.escape(server.name)}`)
171
176
  ?.addEventListener("change", (ev) => _toggle(server.name, ev.target.checked));
172
177
 
173
- if (isExpanded && !server.disabled) _renderTools(server.name);
174
-
175
178
  return card;
176
179
  }
177
180