openclacky 1.5.7 → 1.5.8

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 +33 -0
  3. data/README.md +2 -2
  4. data/README_CN.md +1 -1
  5. data/README_JA.md +1 -1
  6. data/lib/clacky/agent.rb +2 -3
  7. data/lib/clacky/agent_config.rb +40 -18
  8. data/lib/clacky/cli.rb +0 -1
  9. data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +23 -10
  10. data/lib/clacky/default_extensions/ext-studio/skills/ext-develop/SKILL.md +3 -2
  11. data/lib/clacky/default_extensions/general/ext.yml +1 -1
  12. data/lib/clacky/extension/api_loader.rb +4 -11
  13. data/lib/clacky/extension/packager.rb +1 -1
  14. data/lib/clacky/extension/verifier.rb +1 -1
  15. data/lib/clacky/media/generator.rb +3 -3
  16. data/lib/clacky/message_history.rb +1 -1
  17. data/lib/clacky/providers.rb +2 -1
  18. data/lib/clacky/server/http_server.rb +60 -76
  19. data/lib/clacky/session_manager.rb +7 -0
  20. data/lib/clacky/ui2/components/inline_input.rb +1 -0
  21. data/lib/clacky/ui2/components/input_area.rb +1 -0
  22. data/lib/clacky/ui2/components/modal_component.rb +0 -1
  23. data/lib/clacky/ui2/components/todo_area.rb +1 -0
  24. data/lib/clacky/ui2/line_editor.rb +1 -0
  25. data/lib/clacky/ui2/markdown_renderer.rb +5 -1
  26. data/lib/clacky/ui2/ui_controller.rb +1 -0
  27. data/lib/clacky/version.rb +1 -1
  28. data/lib/clacky/web/app.css +134 -210
  29. data/lib/clacky/web/components/chat-navigator.js +1 -1
  30. data/lib/clacky/web/features/billing/store.js +3 -3
  31. data/lib/clacky/web/features/billing/view.js +5 -61
  32. data/lib/clacky/web/features/extensions/store.js +1 -1
  33. data/lib/clacky/web/features/extensions/view.js +28 -5
  34. data/lib/clacky/web/features/new-session/view.js +24 -0
  35. data/lib/clacky/web/i18n.js +20 -18
  36. data/lib/clacky/web/index.html +12 -13
  37. data/lib/clacky/web/projects.js +73 -282
  38. data/lib/clacky/web/settings.js +6 -1
  39. metadata +1 -1
@@ -335,6 +335,13 @@ module Clacky
335
335
  # sessions are never deleted and do not count toward the cap.
336
336
  # Returns count of soft-deleted sessions.
337
337
  def cleanup_by_count(keep:, grouped_keep: 200)
338
+ # Fast path: if total session files can't exceed any single group's cap,
339
+ # skip the expensive all_sessions() full-scan (read + JSON.parse every file).
340
+ # min() is correct because groups are mutually exclusive — any single
341
+ # group's count ≤ non_pinned ≤ file_count ≤ min(keep, grouped_keep).
342
+ file_count = Dir.glob(File.join(@sessions_dir, "*.json")).size
343
+ return 0 if file_count <= [keep, grouped_keep].min
344
+
338
345
  non_pinned = all_sessions.reject { |s| s[:pinned] }
339
346
 
340
347
  groups = non_pinned.group_by do |s|
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "tty-screen"
3
4
  require_relative "../line_editor"
4
5
 
5
6
  module Clacky
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "pastel"
4
+ require "tty-screen"
4
5
  require "tempfile"
5
6
  require_relative "../theme_manager"
6
7
  require_relative "../line_editor"
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'io/console'
4
- require 'tty-prompt'
5
4
  require_relative 'base_component'
6
5
 
7
6
  module Clacky
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "pastel"
4
+ require "tty-screen"
4
5
 
5
6
  module Clacky
6
7
  module UI2
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "pastel"
4
+ require "tty-screen"
4
5
 
5
6
  module Clacky
6
7
  module UI2
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "tty-markdown"
3
+ require "tty-screen"
4
4
  require_relative "theme_manager"
5
5
 
6
6
  module Clacky
@@ -14,6 +14,10 @@ module Clacky
14
14
  def render(content)
15
15
  return content if content.nil? || content.empty?
16
16
 
17
+ # tty-markdown pulls in rouge + kramdown (~0.4s). Server mode renders
18
+ # markdown in the browser, so only pay that cost on a real terminal render.
19
+ require "tty-markdown" unless defined?(TTY::Markdown)
20
+
17
21
  # Get current theme colors
18
22
  theme = ThemeManager.current_theme
19
23
 
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "pastel"
3
4
  require_relative "layout_manager"
4
5
  require_relative "view_renderer"
5
6
  require_relative "progress_handle"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Clacky
4
- VERSION = "1.5.7"
4
+ VERSION = "1.5.8"
5
5
  end
@@ -30,7 +30,7 @@
30
30
  --color-border-strong: #d4d4ce; /* hover / active outline */
31
31
 
32
32
  /* Text — 4-tier hierarchy */
33
- --color-text-primary: #1a1b1e; /* titles, important content */
33
+ --color-text-primary: #344054; /* titles, important content */
34
34
  --color-text-secondary: #56585e; /* body text */
35
35
  --color-text-tertiary: #8a8d94; /* captions, meta */
36
36
  --color-text-muted: #a8abb2; /* placeholders, disabled */
@@ -55,6 +55,7 @@
55
55
  --color-warning: #f59e0b;
56
56
  --color-warning-bg: #fffbeb;
57
57
  --color-warning-border: #fcd34d;
58
+ --color-warning-text: #92400e;
58
59
 
59
60
  --color-info: #0ea5e9;
60
61
  --color-secondary: #10b981;
@@ -113,7 +114,7 @@
113
114
  --color-border-secondary: #1e2229;
114
115
  --color-border-strong: #3a3f4a;
115
116
 
116
- --color-text-primary: #e8eaee;
117
+ --color-text-primary: #c2cad1;
117
118
  --color-text-secondary: #a4a9b4;
118
119
  --color-text-tertiary: #6b707b;
119
120
  --color-text-muted: #565b66;
@@ -137,6 +138,7 @@
137
138
  --color-warning: #fbbf24;
138
139
  --color-warning-bg: #2d1f00;
139
140
  --color-warning-border: #4d3200;
141
+ --color-warning-text: #fcd34d;
140
142
 
141
143
  --color-info: #38bdf8;
142
144
  --color-secondary: #34d399;
@@ -190,6 +192,7 @@
190
192
  --color-warning: #fbbf24;
191
193
  --color-warning-bg: #2d1f00;
192
194
  --color-warning-border: #4d3200;
195
+ --color-warning-text: #fcd34d;
193
196
 
194
197
  --color-info: #38bdf8;
195
198
  --color-secondary: #34d399;
@@ -242,6 +245,7 @@
242
245
  --color-warning: #f59e0b;
243
246
  --color-warning-bg: #fffbeb;
244
247
  --color-warning-border: #fcd34d;
248
+ --color-warning-text: #92400e;
245
249
 
246
250
  --color-info: #0ea5e9;
247
251
  --color-secondary: #10b981;
@@ -294,6 +298,7 @@
294
298
  --color-warning: #fbbf24;
295
299
  --color-warning-bg: #2d1f00;
296
300
  --color-warning-border: #4d3200;
301
+ --color-warning-text: #fcd34d;
297
302
 
298
303
  --color-info: #38bdf8;
299
304
  --color-secondary: #34d399;
@@ -807,7 +812,11 @@ body {
807
812
  margin-bottom: 6px;
808
813
  border-bottom: 1px solid var(--color-border-secondary);
809
814
  }
810
- #ext-slot-sidebar-nav { padding: 10px 0 0px; }
815
+ #ext-slot-sidebar-nav {
816
+ margin-top: 10px;
817
+ padding: 10px 0 0px;
818
+ border-top: 1px solid var(--color-border-secondary);
819
+ }
811
820
  #ext-slot-sidebar-nav-bottom {
812
821
  padding: 8px 0 10px;
813
822
  margin-top: 6px;
@@ -821,7 +830,7 @@ body {
821
830
  gap: 8px;
822
831
  padding: 14px 8px 7px;
823
832
  font-family: var(--font-mono);
824
- font-size: 0.65625rem;
833
+ font-size: 0.75rem;
825
834
  font-weight: 500;
826
835
  color: var(--color-text-tertiary);
827
836
  text-transform: uppercase;
@@ -1511,8 +1520,7 @@ body {
1511
1520
  display: flex;
1512
1521
  align-items: center;
1513
1522
  gap: 0.25rem;
1514
- color: var(--color-text-primary);
1515
- font-size: 0.8125rem;
1523
+ font-size: 0.8325rem;
1516
1524
  font-weight: 400;
1517
1525
  transition: color var(--transition-fast), font-weight var(--transition-fast);
1518
1526
  }
@@ -1805,7 +1813,7 @@ body {
1805
1813
  opacity: 1;
1806
1814
  }
1807
1815
  .task-item.active {
1808
- background: var(--color-accent-soft);
1816
+ background: var(--color-bg-hover);
1809
1817
  }
1810
1818
  .task-item.active::before {
1811
1819
  content: "";
@@ -2802,7 +2810,7 @@ body {
2802
2810
  position: absolute;
2803
2811
  top: 0.5rem;
2804
2812
  right: 0.5rem;
2805
- z-index: 8;
2813
+ z-index: 100;
2806
2814
  display: inline-flex;
2807
2815
  align-items: center;
2808
2816
  justify-content: center;
@@ -7982,6 +7990,29 @@ body {
7982
7990
  border-radius: 4px;
7983
7991
  padding: 0.0625rem 0.375rem;
7984
7992
  }
7993
+ .extension-verified {
7994
+ font-size: 0.625rem;
7995
+ color: var(--color-success, #16a34a);
7996
+ background: var(--color-success-bg, rgba(22,163,74,0.12));
7997
+ border-radius: 4px;
7998
+ padding: 0.2rem 0.375rem;
7999
+ }
8000
+ .extension-unverified-banner {
8001
+ font-size: 0.75rem;
8002
+ color: var(--color-warning-text, #92400e);
8003
+ background: var(--color-warning-bg, #fffbeb);
8004
+ border: 1px solid var(--color-warning-border, #fcd34d);
8005
+ border-radius: 8px;
8006
+ padding: 0.5rem 0.75rem;
8007
+ line-height: 1.4;
8008
+ display: flex;
8009
+ align-items: center;
8010
+ gap: 0.5rem;
8011
+ }
8012
+ .extension-unverified-icon {
8013
+ flex-shrink: 0;
8014
+ color: var(--color-warning, #d97706);
8015
+ }
7985
8016
  .extension-desc {
7986
8017
  font-size: 0.75rem;
7987
8018
  color: var(--color-text-secondary);
@@ -11549,6 +11580,42 @@ body.setup-mode[data-theme="dark"] {
11549
11580
  gap: 0.625rem;
11550
11581
  }
11551
11582
 
11583
+ /* ── Segmented Control (reusable compact tab selector) ──────────────────── */
11584
+ .segmented-control {
11585
+ display: flex;
11586
+ gap: 2px;
11587
+ background: var(--color-bg-hover);
11588
+ border-radius: 8px;
11589
+ padding: 3px;
11590
+ }
11591
+ .segmented-control__btn {
11592
+ padding: 0.3125rem 0.75rem;
11593
+ border: none;
11594
+ background: transparent;
11595
+ color: var(--color-text-secondary);
11596
+ font-size: 0.8125rem;
11597
+ cursor: pointer;
11598
+ border-radius: 6px;
11599
+ transition: all 0.15s ease;
11600
+ white-space: nowrap;
11601
+ }
11602
+ .segmented-control__btn:hover:not(.active) {
11603
+ color: var(--color-text-primary);
11604
+ }
11605
+ .segmented-control__btn.active {
11606
+ background: var(--color-bg-secondary);
11607
+ color: var(--color-text-primary);
11608
+ font-weight: 500;
11609
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08), 0 1px 2px rgba(0, 0, 0, 0.06);
11610
+ }
11611
+ [data-theme="dark"] .segmented-control {
11612
+ background: var(--color-bg-primary);
11613
+ }
11614
+ [data-theme="dark"] .segmented-control__btn.active {
11615
+ background: var(--color-bg-hover);
11616
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4);
11617
+ }
11618
+
11552
11619
  /* ═════════════════════════════════════════════════════════════════════════
11553
11620
  * BILLING PANEL
11554
11621
  * ═════════════════════════════════════════════════════════════════════════ */
@@ -11636,39 +11703,9 @@ body.setup-mode[data-theme="dark"] {
11636
11703
  border-radius: 6px;
11637
11704
  }
11638
11705
 
11639
- /* ── Period Button Group ─────────────────────────────────────────────── */
11640
- .billing-period-group {
11641
- display: flex;
11642
- background: var(--color-bg-secondary);
11643
- border: 1px solid var(--color-border-primary);
11644
- border-radius: 8px;
11645
- overflow: hidden;
11646
- }
11647
- .billing-period-btn {
11648
- padding: 0.4375rem 0.75rem;
11649
- border: none;
11650
- background: transparent;
11651
- color: var(--color-text-secondary);
11652
- font-size: 0.8125rem;
11653
- cursor: pointer;
11654
- transition: all 0.15s ease;
11655
- }
11656
- .billing-period-btn:not(:last-child) {
11657
- border-right: 1px solid var(--color-border-primary);
11658
- }
11659
- .billing-period-btn:hover {
11660
- background: var(--color-bg-hover);
11661
- color: var(--color-text-primary);
11662
- }
11663
- .billing-period-btn.active {
11664
- background: var(--color-accent-primary);
11665
- color: #ffffff;
11666
- font-weight: 500;
11667
- }
11668
-
11669
11706
  /* ── Model Filter ────────────────────────────────────────────────────── */
11670
11707
  .billing-model-filter {
11671
- padding: 0.4375rem 1.75rem 0.4375rem 0.75rem;
11708
+ padding: 0.5375rem 1.75rem 0.5375rem 0.75rem;
11672
11709
  border: 1px solid var(--color-border-primary);
11673
11710
  border-radius: 8px;
11674
11711
  background: var(--color-bg-secondary);
@@ -11693,28 +11730,6 @@ body.setup-mode[data-theme="dark"] {
11693
11730
  }
11694
11731
 
11695
11732
  /* ── Action Buttons ──────────────────────────────────────────────────── */
11696
- .billing-clear-container {
11697
- position: relative;
11698
- }
11699
- .billing-clear-btn {
11700
- width: 32px;
11701
- height: 32px;
11702
- display: inline-flex;
11703
- align-items: center;
11704
- justify-content: center;
11705
- border: 1px solid var(--color-border-primary);
11706
- border-radius: 8px;
11707
- background: var(--color-bg-secondary);
11708
- color: var(--color-text-secondary);
11709
- font-size: 0.875rem;
11710
- cursor: pointer;
11711
- transition: all 0.15s ease;
11712
- }
11713
- .billing-clear-btn:hover {
11714
- background: var(--color-bg-tertiary);
11715
- border-color: #ef4444;
11716
- color: #ef4444;
11717
- }
11718
11733
  .billing-share-btn {
11719
11734
  display: inline-flex;
11720
11735
  align-items: center;
@@ -11735,43 +11750,6 @@ body.setup-mode[data-theme="dark"] {
11735
11750
  color: var(--color-text-primary);
11736
11751
  border-color: var(--color-accent-primary);
11737
11752
  }
11738
- .billing-clear-popup {
11739
- position: absolute;
11740
- top: 100%;
11741
- right: 0;
11742
- margin-top: 0.25rem;
11743
- display: flex;
11744
- flex-direction: column;
11745
- gap: 0.25rem;
11746
- background: var(--color-bg-secondary);
11747
- border: 1px solid var(--color-border-primary);
11748
- border-radius: 8px;
11749
- padding: 0.375rem;
11750
- box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
11751
- z-index: 100;
11752
- min-width: 120px;
11753
- }
11754
- .billing-clear-option {
11755
- padding: 0.4375rem 0.75rem;
11756
- border: none;
11757
- border-radius: 6px;
11758
- background: transparent;
11759
- color: var(--color-text-primary);
11760
- font-size: 0.8125rem;
11761
- cursor: pointer;
11762
- text-align: left;
11763
- white-space: nowrap;
11764
- transition: background 0.15s ease;
11765
- }
11766
- .billing-clear-option:hover {
11767
- background: var(--color-bg-tertiary);
11768
- }
11769
- .billing-clear-danger {
11770
- color: #ef4444;
11771
- }
11772
- .billing-clear-danger:hover {
11773
- background: rgba(239, 68, 68, 0.08) !important;
11774
- }
11775
11753
 
11776
11754
  /* ── Stats Row ───────────────────────────────────────────────────────── */
11777
11755
  .billing-stats-row {
@@ -11901,11 +11879,6 @@ body.setup-mode[data-theme="dark"] {
11901
11879
  }
11902
11880
 
11903
11881
  /* ── Cost Trend Line Chart ───────────────────────────────────────────── */
11904
- .billing-trend-total {
11905
- font-size: 0.75rem;
11906
- font-weight: 500;
11907
- color: var(--color-accent-primary);
11908
- }
11909
11882
  .billing-trend-chart {
11910
11883
  flex: 1;
11911
11884
  min-height: 0;
@@ -14050,11 +14023,23 @@ body.setup-mode[data-theme="dark"] {
14050
14023
  }
14051
14024
 
14052
14025
  /* ── Section-level collapse ────────────────────────────────────────────────── */
14026
+ #projects-section-header {
14027
+ cursor: pointer;
14028
+ padding: 0.25rem 0.5rem;
14029
+ margin-top: 0.5rem;
14030
+ border-radius: var(--radius-sm);
14031
+ transition: background var(--transition-fast);
14032
+ }
14033
+ #projects-section-header:hover {
14034
+ background: var(--color-bg-hover);
14035
+ }
14036
+ #projects-section-header:hover .btn-icon-sm:hover {
14037
+ background: var(--color-border-primary);
14038
+ }
14053
14039
  .projects-section-title {
14054
14040
  display: inline-flex;
14055
14041
  align-items: center;
14056
14042
  gap: 3px;
14057
- cursor: pointer;
14058
14043
  }
14059
14044
  .projects-section-chevron {
14060
14045
  flex-shrink: 0;
@@ -14078,8 +14063,8 @@ body.setup-mode[data-theme="dark"] {
14078
14063
  .project-group-header {
14079
14064
  display: flex;
14080
14065
  align-items: center;
14081
- gap: 0.375rem;
14082
- padding: 0.3125rem 0.625rem;
14066
+ gap: 0.575rem;
14067
+ padding: 0.3325rem 0.625rem;
14083
14068
  border-radius: var(--radius-sm);
14084
14069
  cursor: pointer;
14085
14070
  user-select: none;
@@ -14095,7 +14080,7 @@ body.setup-mode[data-theme="dark"] {
14095
14080
  overflow: hidden;
14096
14081
  text-overflow: ellipsis;
14097
14082
  white-space: nowrap;
14098
- font-size: 0.8125rem;
14083
+ font-size: 0.875rem;
14099
14084
  color: var(--color-text-primary);
14100
14085
  }
14101
14086
 
@@ -14134,113 +14119,18 @@ body.setup-mode[data-theme="dark"] {
14134
14119
  margin-top: 0.08rem;
14135
14120
  }
14136
14121
 
14137
- .project-sessions-empty {
14138
- padding: 0.25rem 0.5rem 0.25rem 0.625rem;
14139
- font-size: 0.75rem;
14140
- color: var(--color-text-muted);
14141
- }
14142
-
14143
- /* ── Project hover tooltip ─────────────────────────────────────────────────── */
14144
- .project-tooltip {
14145
- position: fixed;
14146
- z-index: 1200;
14147
- background: var(--color-bg-secondary);
14148
- border: 1px solid var(--color-border-primary);
14149
- border-radius: 10px;
14150
- box-shadow: 0 8px 24px rgba(0,0,0,.10), 0 2px 8px rgba(0,0,0,.06);
14151
- padding: 0.75rem 0.875rem 0.75rem 1.125rem; /* extra left padding = visual gap */
14152
- min-width: 13rem;
14153
- max-width: 18rem;
14154
- animation: fadeIn 0.12s ease;
14155
- }
14156
- /* Transparent bridge on the left side fills the gap between sidebar and tooltip */
14157
- .project-tooltip::before {
14158
- content: "";
14159
- position: absolute;
14160
- top: 0;
14161
- left: -8px;
14162
- width: 8px;
14163
- height: 100%;
14164
- }
14165
- [data-theme="dark"] .project-tooltip {
14166
- box-shadow: 0 8px 28px rgba(0,0,0,.55), 0 2px 8px rgba(0,0,0,.35);
14167
- }
14168
-
14169
- .project-tooltip-header {
14170
- display: flex;
14171
- align-items: center;
14172
- gap: 0.4rem;
14173
- margin-bottom: 0.75rem;
14174
- }
14175
- .project-tooltip-icon {
14176
- flex-shrink: 0;
14177
- display: flex;
14178
- align-items: center;
14179
- justify-content: center;
14180
- color: var(--color-text-secondary);
14181
- line-height: 0;
14182
- }
14183
- .project-tooltip-icon svg {
14184
- display: block;
14185
- flex-shrink: 0;
14186
- }
14187
- .project-tooltip-name {
14188
- flex: 1;
14189
- min-width: 0;
14190
- font-size: 0.8125rem;
14191
- font-weight: 500;
14192
- color: var(--color-text-primary);
14193
- overflow: hidden;
14194
- text-overflow: ellipsis;
14195
- white-space: nowrap;
14196
- }
14197
- .project-tooltip-rows {
14198
- display: flex;
14199
- flex-direction: column;
14200
- gap: 0.3rem;
14201
- }
14202
- .project-tooltip-row {
14203
- display: flex;
14204
- align-items: center;
14205
- gap: 0.4rem;
14206
- font-size: 0.75rem;
14207
- color: var(--color-text-secondary);
14208
- min-width: 0;
14209
- }
14210
- .project-tooltip-row--clickable {
14211
- cursor: pointer;
14212
- border-radius: 4px;
14213
- margin: 0 -0.25rem;
14214
- padding: 0.15rem 0.25rem;
14215
- transition: background var(--transition-fast);
14216
- }
14217
- .project-tooltip-row--clickable:hover {
14122
+ .project-session-group .session-item.active {
14218
14123
  background: var(--color-bg-hover);
14219
14124
  }
14220
14125
 
14221
- .project-tooltip-divider {
14222
- height: 1px;
14223
- background: var(--color-border-primary);
14224
- margin: 0.25rem 0;
14126
+ .project-session-group .session-item.active::before {
14127
+ display: none;
14225
14128
  }
14226
14129
 
14227
- .project-tooltip-row-icon {
14228
- flex-shrink: 0;
14130
+ .project-sessions-empty {
14131
+ padding: 0.25rem 0.5rem 0.25rem 0.625rem;
14132
+ font-size: 0.75rem;
14229
14133
  color: var(--color-text-muted);
14230
- display: flex;
14231
- align-items: center;
14232
- line-height: 0;
14233
- }
14234
- .project-tooltip-row-icon svg {
14235
- display: block;
14236
- flex-shrink: 0;
14237
- }
14238
- .project-tooltip-row-text {
14239
- min-width: 0;
14240
- overflow: hidden;
14241
- text-overflow: ellipsis;
14242
- white-space: nowrap;
14243
- line-height: 1.4;
14244
14134
  }
14245
14135
 
14246
14136
  /* ── Project context menu (rename / delete) ────────────────────────────────── */
@@ -14258,7 +14148,9 @@ body.setup-mode[data-theme="dark"] {
14258
14148
  box-shadow: 0 8px 24px rgba(0,0,0,.5), 0 2px 6px rgba(0,0,0,.3);
14259
14149
  }
14260
14150
  .project-actions-menu-item {
14261
- display: block;
14151
+ display: flex;
14152
+ align-items: center;
14153
+ gap: 0.5rem;
14262
14154
  width: 100%;
14263
14155
  padding: 0.375rem 0.625rem;
14264
14156
  border: none;
@@ -14267,17 +14159,43 @@ body.setup-mode[data-theme="dark"] {
14267
14159
  cursor: pointer;
14268
14160
  color: var(--color-text-primary);
14269
14161
  font-size: 0.8125rem;
14162
+ line-height: 1.4;
14270
14163
  text-align: left;
14271
14164
  transition: background .12s ease, color .12s ease;
14272
14165
  }
14166
+ .project-actions-menu-icon {
14167
+ display: inline-flex;
14168
+ align-items: center;
14169
+ justify-content: center;
14170
+ width: 0.875rem;
14171
+ height: 0.875rem;
14172
+ flex: 0 0 0.875rem;
14173
+ color: var(--color-text-secondary);
14174
+ transition: color .12s ease;
14175
+ }
14176
+ .project-actions-menu-icon svg {
14177
+ display: block;
14178
+ flex-shrink: 0;
14179
+ }
14180
+ .project-actions-menu-label {
14181
+ min-width: 0;
14182
+ flex: 1;
14183
+ }
14273
14184
  .project-actions-menu-item:hover {
14274
14185
  background: var(--color-bg-hover);
14275
14186
  }
14187
+ .project-actions-menu-item:hover .project-actions-menu-icon {
14188
+ color: var(--color-text-primary);
14189
+ }
14276
14190
  .project-actions-menu-item.danger {
14277
14191
  color: var(--color-error);
14278
14192
  margin-top: 0.25rem;
14279
14193
  position: relative;
14280
14194
  }
14195
+ .project-actions-menu-item.danger .project-actions-menu-icon,
14196
+ .project-actions-menu-item.danger:hover .project-actions-menu-icon {
14197
+ color: var(--color-error);
14198
+ }
14281
14199
  .project-actions-menu-item.danger::before {
14282
14200
  content: "";
14283
14201
  position: absolute;
@@ -14406,6 +14324,13 @@ body.setup-mode[data-theme="dark"] {
14406
14324
  margin-top: 0.75rem;
14407
14325
  }
14408
14326
 
14327
+ .create-project-folder-hint {
14328
+ font-size: 0.72rem;
14329
+ color: var(--color-text-muted);
14330
+ margin-top: 0.35rem;
14331
+ line-height: 1.4;
14332
+ }
14333
+
14409
14334
  .create-project-folder-label {
14410
14335
  font-size: 0.78rem;
14411
14336
  font-weight: 500;
@@ -14603,4 +14528,3 @@ body.setup-mode[data-theme="dark"] {
14603
14528
  .project-picker-done-btn:hover {
14604
14529
  background: var(--color-bg-hover);
14605
14530
  }
14606
-
@@ -163,7 +163,7 @@
163
163
  popup.style.bottom = "";
164
164
  const popupHeight = popup.offsetHeight;
165
165
  let top = barCenter - popupHeight / 2;
166
- top = Math.max(0, Math.min(top, navRect.height - popupHeight));
166
+ top = Math.max(48, Math.min(top, navRect.height - popupHeight));
167
167
  popup.style.top = top + "px";
168
168
 
169
169
  const target = items[idx].popupItem;
@@ -2,7 +2,7 @@
2
2
  //
3
3
  // Owns billing state (summary / daily / sessions / model list / current period
4
4
  // & model filter), currency conversion utilities (localStorage-backed), the
5
- // data fetch, and clear-data actions. It never renders.
5
+ // data fetch, and clear-data actions (retained for API use). It never renders.
6
6
  //
7
7
  // Currency utilities are pure and read by both the view and other modules, so
8
8
  // they live with the data layer. Emits store events the view reacts to; mirrors
@@ -103,8 +103,8 @@ const BillingStore = (() => {
103
103
  if (_summary) _emit("billing:changed");
104
104
  },
105
105
 
106
- setPeriod(period) { _currentPeriod = period; return Billing.load(); },
107
- setModel(model) { _currentModel = model; return Billing.load(); },
106
+ setPeriod(period) { _currentPeriod = period; _emit("billing:changed"); return Billing.load(); },
107
+ setModel(model) { _currentModel = model; _emit("billing:changed"); return Billing.load(); },
108
108
 
109
109
  async load() {
110
110
  const isFirstLoad = !_summary;