openclacky 1.3.0 → 1.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8423d4e64f056251f1763e5cc34502e394f08e2b26de993da5da6a85a88111ef
4
- data.tar.gz: 62e60aee3f8654e881870117b4716e053ab0f9328bc3898392ff1ed27517474c
3
+ metadata.gz: ed703cddac3ffa9887b5c500c5dfbe57c895eb5aab0e9dc3338592c9317555ec
4
+ data.tar.gz: 67d21c3c857e72bb729cfcecbdb3749eaaac1c76b21e1d0600081d1e4d79e880
5
5
  SHA512:
6
- metadata.gz: d675c6b981a1fc5f24bbee79dd19dc273817a7f4e233c2ed86a42def4f2fd2876cbc845a9f693383d7f4b67d39f6a62df1eafa65def2a8ace7bb876559c6aba5
7
- data.tar.gz: 48adb156c4c8ab26d2908537ac409c161a56b1f6fa5aa2146bdaa5df6b11c2b333e5a04a6aafc4404f1c94ce210fe0c23854d0f86f9cc509f1a27a781f9aa007
6
+ metadata.gz: 4fe46c92c67e5937bf24e325be47046a9ebf555afb722aaaaf108a17b2c13403c7e42c08d4b1a7abafad5c5ffb5d79d2749264c051572d157d0094dcb067ac78
7
+ data.tar.gz: 5fa8e909159560aa13944c0435716e7a98265504c6f61e8cfc298bc09553aa218ac404423d4e9b45f5a8d17563f0ae8a7a50547460fb497e06a98bc46b55aa53
data/CHANGELOG.md CHANGED
@@ -5,6 +5,36 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.3.2] - 2026-06-18
9
+
10
+ ### Added
11
+ - Right-click context menu on workspace file tree with "Reveal in Finder" support (macOS/Linux/WSL)
12
+ - Resizable workspace panel with a drag handle
13
+ - Expandable task prompt preview — click a task card to view the full prompt
14
+
15
+ ### Improved
16
+ - Skill cards now show a delete button, with shimmer skeleton loading while fetching
17
+
18
+ ### Fixed
19
+ - Every-N-hours cron schedule label now renders correctly (e.g. `0 */2 * * *`)
20
+ - Billing sessions list now shows all rows instead of being clipped
21
+ - Session replay skips reasoning-only assistant messages to match the live view
22
+ - Set-as-default button in settings aligned to the footer row with the top-up link
23
+
24
+ ## [1.3.1] - 2026-06-17
25
+
26
+ ### Added
27
+ - Delete skills directly from the Web UI
28
+
29
+ ### Improved
30
+ - Sidebar is now draggable to resize, with width persisted across sessions
31
+ - Sidebar scrollbar only appears while scrolling for a cleaner look
32
+ - Billing page UI polish and mobile adaptation
33
+ - Default openclacky image model switched to Nano Banana 2
34
+
35
+ ### Fixed
36
+ - Onboarding device-login window no longer gets blocked as a popup
37
+
8
38
  ## [1.3.0] - 2026-06-17
9
39
 
10
40
  ### Added
@@ -502,14 +502,24 @@ module Clacky
502
502
 
503
503
  case msg[:role].to_s
504
504
  when "assistant"
505
- # Text content prepend reasoning/thinking content wrapped in <think> tags
506
- # so the Web UI renders it as a collapsible thinking block
507
- text = extract_text_from_content(msg[:content]).to_s.strip
505
+ # Mirror the live guard at agent.rb (`if response[:content] && !response[:content].empty?`):
506
+ # only emit an assistant_message when the model produced actual content.
507
+ # Reasoning-only turns (empty content + reasoning_content + tool_calls)
508
+ # are silent in live mode; on replay they must stay silent too — otherwise
509
+ # a phantom <think>-only bubble splits consecutive tool_calls into separate
510
+ # UI groups, breaking the "N tool(s) used" collapse after refresh (C-5672).
511
+ raw_text = extract_text_from_content(msg[:content]).to_s.strip
508
512
  reasoning = msg[:reasoning_content]
509
- if reasoning && !reasoning.to_s.strip.empty?
510
- text = "<think>\n#{reasoning}\n</think>\n#{text}"
513
+ unless raw_text.empty?
514
+ text = if reasoning && !reasoning.to_s.strip.empty?
515
+ # Prepend reasoning wrapped in <think> tags so the Web UI renders it
516
+ # as a collapsible thinking block.
517
+ "<think>\n#{reasoning}\n</think>\n#{raw_text}"
518
+ else
519
+ raw_text
520
+ end
521
+ ui.show_assistant_message(text, files: [])
511
522
  end
512
- ui.show_assistant_message(text, files: []) unless text.empty?
513
523
 
514
524
  # Tool calls embedded in assistant message
515
525
  Array(msg[:tool_calls]).each do |tc|
@@ -961,7 +961,7 @@ module Clacky
961
961
  #
962
962
  # @param skill_name [String] The slug/name of the skill to remove.
963
963
  # @return [void]
964
- private def delete_brand_skill!(skill_name)
964
+ def delete_brand_skill!(skill_name)
965
965
  # Remove files from disk.
966
966
  skill_dir = File.join(brand_skills_dir, skill_name)
967
967
  FileUtils.rm_rf(skill_dir) if Dir.exist?(skill_dir)
@@ -59,7 +59,7 @@ module Clacky
59
59
  "or-gemini-3-1-flash-image" => "Nano Banana 2",
60
60
  "or-gpt-image-2" => "GPT Image 2"
61
61
  },
62
- "default_image_model" => "or-gpt-image-2",
62
+ "default_image_model" => "or-gemini-3-1-flash-image",
63
63
  # Video generation models served by the openclacky gateway, which
64
64
  # routes them to Vertex AI Veo (async predictLongRunning under the
65
65
  # hood; the gateway hides the polling and returns the MP4 inline).
@@ -627,6 +627,12 @@ module Clacky
627
627
  elsif method == "PATCH" && path.match?(%r{^/api/skills/[^/]+/toggle$})
628
628
  name = URI.decode_www_form_component(path.sub("/api/skills/", "").sub("/toggle", ""))
629
629
  api_toggle_skill(name, req, res)
630
+ elsif method == "DELETE" && path.match?(%r{^/api/skills/[^/]+$})
631
+ name = URI.decode_www_form_component(path.sub("/api/skills/", ""))
632
+ api_delete_skill(name, res)
633
+ elsif method == "DELETE" && path.match?(%r{^/api/brand/skills/[^/]+$})
634
+ slug = URI.decode_www_form_component(path.sub("/api/brand/skills/", ""))
635
+ api_delete_brand_skill(slug, res)
630
636
  elsif method == "POST" && path.match?(%r{^/api/brand/skills/[^/]+/install$})
631
637
  slug = URI.decode_www_form_component(path.sub("/api/brand/skills/", "").sub("/install", ""))
632
638
  api_brand_skill_install(slug, req, res)
@@ -1939,6 +1945,23 @@ module Clacky
1939
1945
  json_response(res, 500, { ok: false, error: e.message })
1940
1946
  end
1941
1947
 
1948
+ # DELETE /api/brand/skills/:slug
1949
+ # Uninstalls a brand skill by removing its files and metadata.
1950
+ def api_delete_brand_skill(slug, res)
1951
+ brand = Clacky::BrandConfig.load
1952
+ installed = brand.installed_brand_skills
1953
+ unless installed.key?(slug)
1954
+ json_response(res, 404, { ok: false, error: "Brand skill '#{slug}' is not installed" })
1955
+ return
1956
+ end
1957
+
1958
+ brand.delete_brand_skill!(slug)
1959
+ @skill_loader = Clacky::SkillLoader.new(working_dir: nil, brand_config: brand)
1960
+ json_response(res, 200, { ok: true })
1961
+ rescue StandardError => e
1962
+ json_response(res, 500, { ok: false, error: e.message })
1963
+ end
1964
+
1942
1965
  # GET /api/brand
1943
1966
  # Returns brand metadata consumed by the WebUI on boot
1944
1967
  # to dynamically replace branding strings.
@@ -2892,10 +2915,13 @@ module Clacky
2892
2915
  result = Utils::EnvironmentDetector.open_file(linux_path)
2893
2916
  return json_response(res, 501, { error: "unsupported OS" }) if result.nil?
2894
2917
  json_response(res, 200, { ok: true })
2918
+ when "reveal"
2919
+ Utils::EnvironmentDetector.reveal_file(linux_path)
2920
+ json_response(res, 200, { ok: true })
2895
2921
  when "download"
2896
2922
  serve_file_download(res, linux_path)
2897
2923
  else
2898
- json_response(res, 400, { error: "invalid action. Must be 'open' or 'download'" })
2924
+ json_response(res, 400, { error: "invalid action. Must be 'open', 'reveal' or 'download'" })
2899
2925
  end
2900
2926
  rescue => e
2901
2927
  json_response(res, 500, { ok: false, error: e.message })
@@ -3463,6 +3489,14 @@ module Clacky
3463
3489
  json_response(res, 422, { error: e.message })
3464
3490
  end
3465
3491
 
3492
+ private def api_delete_skill(name, res)
3493
+ skill = @skill_loader[name]
3494
+ return json_response(res, 404, { error: "Skill not found: #{name}" }) unless skill
3495
+
3496
+ FileUtils.rm_rf(skill.directory)
3497
+ json_response(res, 200, { ok: true })
3498
+ end
3499
+
3466
3500
  # POST /api/my-skills/:name/publish
3467
3501
  # GET /api/creator/skills
3468
3502
  # Returns two separate groups:
@@ -36,6 +36,22 @@ module Clacky
36
36
  end
37
37
  end
38
38
 
39
+ # Reveal a file in the OS file manager (select/highlight it).
40
+ # macOS: open -R; Linux: xdg-open on parent dir; WSL: explorer /select
41
+ # @param path [String] Linux-side file path
42
+ # @return [Boolean, nil] true/false from system(), or nil on unsupported OS
43
+ def self.reveal_file(path)
44
+ case os_type
45
+ when :macos then system("open", "-R", path)
46
+ when :linux then system("xdg-open", File.dirname(path))
47
+ when :wsl
48
+ win_path = linux_to_win_path(path)
49
+ system("explorer.exe", "/select,#{win_path}")
50
+ else
51
+ nil
52
+ end
53
+ end
54
+
39
55
  # Convert a Windows-style path to a WSL/Linux-side path.
40
56
  # e.g. "C:/Users/foo/file.txt" → "/mnt/c/Users/foo/file.txt"
41
57
  # Returns the original path unchanged on non-WSL or if already a Linux path.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Clacky
4
- VERSION = "1.3.0"
4
+ VERSION = "1.3.2"
5
5
  end
@@ -250,7 +250,7 @@ html { font-size: 16px; }
250
250
  .skel-label { height: 0.75rem; width: 4rem; margin-top: 0.375rem; }
251
251
  .skel-block { height: 9rem; width: 100%; }
252
252
  .skel-block-sm { height: 5rem; width: 100%; }
253
- .skel-heatmap { height: 7rem; width: 100%; }
253
+ .skel-heatmap { height: 5rem; width: 178px; }
254
254
 
255
255
 
256
256
 
@@ -375,7 +375,7 @@ body {
375
375
  }
376
376
 
377
377
  .header-logo-img {
378
- height: 2.3rem;
378
+ height: 2rem;
379
379
  object-fit: contain;
380
380
  display: block;
381
381
  flex-shrink: 0;
@@ -498,10 +498,6 @@ body {
498
498
  .theme-toggle-btn:active {
499
499
  background: var(--color-bg-hover);
500
500
  }
501
- /* Sound-notification toggle shares .theme-toggle-btn; highlight when ON. */
502
- #notify-toggle-header.notify-on:hover {
503
- color: var(--color-accent-primary, var(--color-text-primary));
504
- }
505
501
 
506
502
  /* ── Content Row (Sidebar + Main) ───────────────────────────────────────── */
507
503
  #app > aside,
@@ -530,8 +526,9 @@ body {
530
526
  /* The sidebar sits on the outer frame (bg-primary) so it's visibly a layer
531
527
  BEHIND the chat surface (bg-secondary). Subtle right border separates them. */
532
528
  #sidebar {
533
- width: 15rem;
534
- min-width: 15rem;
529
+ --sidebar-width: 15rem;
530
+ width: var(--sidebar-width);
531
+ min-width: var(--sidebar-width);
535
532
  background: var(--color-bg-primary);
536
533
  border-right: 1px solid var(--color-border-primary);
537
534
  display: flex;
@@ -540,9 +537,22 @@ body {
540
537
  height: 100%;
541
538
  flex-shrink: 0;
542
539
  margin-left: 0;
540
+ position: relative;
543
541
  }
544
542
  #sidebar.hidden {
545
- margin-left: -15rem;
543
+ margin-left: calc(-1 * var(--sidebar-width));
544
+ }
545
+ #sidebar-resize-handle {
546
+ position: absolute;
547
+ top: 0;
548
+ right: -3px;
549
+ bottom: 0;
550
+ width: 6px;
551
+ cursor: col-resize;
552
+ z-index: 10;
553
+ }
554
+ #sidebar:has(#sidebar-resize-handle.active) {
555
+ transition: background-color var(--transition-base);
546
556
  }
547
557
  #sidebar-header {
548
558
  display: flex;
@@ -578,6 +588,22 @@ body {
578
588
  flex-direction: column;
579
589
  min-height: 0;
580
590
  padding: 0 6px 8px;
591
+ margin-right: -1px;
592
+ scrollbar-width: thin;
593
+ scrollbar-color: transparent transparent;
594
+ }
595
+ #sidebar-list.is-scrolling {
596
+ scrollbar-color: var(--color-border-secondary) transparent;
597
+ }
598
+ #sidebar-list::-webkit-scrollbar { width: 4px; }
599
+ #sidebar-list::-webkit-scrollbar-track { background: transparent; }
600
+ #sidebar-list::-webkit-scrollbar-thumb {
601
+ background: transparent;
602
+ border-radius: 4px;
603
+ transition: background 0.4s;
604
+ }
605
+ #sidebar-list.is-scrolling::-webkit-scrollbar-thumb {
606
+ background: var(--color-border-secondary);
581
607
  }
582
608
  #session-list { padding: 4px 0 0; min-height: 6.75rem; }
583
609
 
@@ -598,10 +624,6 @@ body {
598
624
  background: var(--color-bg-primary);
599
625
  z-index: 10;
600
626
  }
601
- .sidebar-divider:first-child {
602
- margin-top: 0;
603
- padding-top: 18px;
604
- }
605
627
  .sidebar-divider span {
606
628
  white-space: nowrap;
607
629
  flex: 0 0 auto;
@@ -1732,6 +1754,30 @@ body {
1732
1754
  overflow: hidden;
1733
1755
  text-overflow: ellipsis;
1734
1756
  }
1757
+ .task-card-preview-expandable {
1758
+ cursor: pointer;
1759
+ }
1760
+ .task-card-preview-expandable:hover {
1761
+ color: var(--color-accent-primary);
1762
+ }
1763
+ .task-card-detail {
1764
+ margin-top: 0.625rem;
1765
+ padding: 0.625rem 0.75rem;
1766
+ background: var(--color-bg-primary);
1767
+ border: 1px solid var(--color-border-primary);
1768
+ border-radius: 6px;
1769
+ }
1770
+ .task-card-detail-content {
1771
+ margin: 0;
1772
+ font-size: 0.75rem;
1773
+ font-family: inherit;
1774
+ color: var(--color-text-secondary);
1775
+ line-height: 1.6;
1776
+ white-space: pre-wrap;
1777
+ word-break: break-word;
1778
+ max-height: 12rem;
1779
+ overflow-y: auto;
1780
+ }
1735
1781
  .task-card-actions {
1736
1782
  display: flex;
1737
1783
  align-items: center;
@@ -1885,7 +1931,8 @@ body {
1885
1931
 
1886
1932
  /* ── Workspace panel (right file browser) ───────────────────────────────── */
1887
1933
  #workspace-panel {
1888
- width: 280px;
1934
+ --workspace-width: 280px;
1935
+ width: var(--workspace-width);
1889
1936
  flex-shrink: 0;
1890
1937
  display: flex;
1891
1938
  flex-direction: column;
@@ -1893,8 +1940,21 @@ body {
1893
1940
  background: var(--color-bg-primary);
1894
1941
  overflow: hidden;
1895
1942
  transition: width var(--transition-base);
1943
+ position: relative;
1896
1944
  }
1897
1945
  #workspace-panel.collapsed { width: 0; border-left: none; }
1946
+ #workspace-resize-handle {
1947
+ position: absolute;
1948
+ top: 0;
1949
+ left: -3px;
1950
+ bottom: 0;
1951
+ width: 6px;
1952
+ cursor: col-resize;
1953
+ z-index: 10;
1954
+ }
1955
+ #workspace-panel:has(#workspace-resize-handle.active) {
1956
+ transition: background-color var(--transition-base);
1957
+ }
1898
1958
  #workspace-header {
1899
1959
  display: flex;
1900
1960
  align-items: center;
@@ -2128,7 +2188,7 @@ body {
2128
2188
  .msg-user .msg-time { color: var(--color-text-secondary); right: 0; left: auto; padding-right: 0.25rem; }
2129
2189
  .msg-assistant .msg-time { color: var(--color-text-secondary); left: 0; right: auto; padding-left: 0.25rem; }
2130
2190
 
2131
- .msg-user { background: var(--color-accent-soft); color: var(--color-text-primary); align-self: flex-end; white-space: pre-wrap; border: 1px solid var(--color-border-secondary); }
2191
+ .msg-user { background: var(--color-accent-soft); color: var(--color-text-primary); align-self: flex-end; white-space: pre-wrap; }
2132
2192
  [data-theme="dark"] .msg-user { background: var(--color-accent-soft); }
2133
2193
  .msg-assistant { background: var(--color-bg-tertiary); border: 1px solid var(--color-border-primary); align-self: flex-start; }
2134
2194
 
@@ -2777,11 +2837,11 @@ body {
2777
2837
  #session-info-bar {
2778
2838
  display: flex;
2779
2839
  align-items: center;
2780
- gap: 6px;
2840
+ gap: 0.375rem;
2781
2841
  flex-wrap: wrap;
2782
- padding: 6px 0 6px 0;
2783
- margin-left: 24px;
2784
- margin-right: 24px;
2842
+ padding: 0.375rem 0.75rem;
2843
+ margin-left: 1.5rem;
2844
+ margin-right: 1.5rem;
2785
2845
  background: transparent;
2786
2846
  border-top: none;
2787
2847
  font-size: 0.6875rem;
@@ -4611,14 +4671,15 @@ body {
4611
4671
  }
4612
4672
  .model-card-grid-toolbar {
4613
4673
  display: flex;
4614
- flex-wrap: wrap;
4674
+ flex-wrap: nowrap;
4615
4675
  gap: 0.375rem;
4616
4676
  margin-left: auto;
4617
4677
  justify-content: flex-end;
4618
4678
  }
4619
4679
  .model-card-grid-footer {
4620
4680
  display: flex;
4621
- justify-content: flex-end;
4681
+ justify-content: space-between;
4682
+ align-items: center;
4622
4683
  margin-top: 0.25rem;
4623
4684
  }
4624
4685
  .model-card-grid-link {
@@ -6541,6 +6602,24 @@ body {
6541
6602
  background: var(--color-success-bg);
6542
6603
  }
6543
6604
 
6605
+ /* ── Delete button ────────────────────────────────────────────────────────── */
6606
+ .btn-skill-delete {
6607
+ background: transparent;
6608
+ border: 1px solid transparent;
6609
+ border-radius: 6px;
6610
+ color: var(--color-text-tertiary);
6611
+ cursor: pointer;
6612
+ padding: 0.25rem;
6613
+ display: inline-flex;
6614
+ align-items: center;
6615
+ transition: color .15s, border-color .15s, background .15s;
6616
+ }
6617
+ .btn-skill-delete:hover {
6618
+ color: var(--color-error, #c0392b);
6619
+ border-color: var(--color-error-border, #f5c6c6);
6620
+ background: var(--color-error-bg, #fff0f0);
6621
+ }
6622
+
6544
6623
  /* ── Skills sidebar section ──────────────────────────────────────────────── */
6545
6624
  #skill-list-items { padding: 0 0.5rem 0.5rem; display: flex; flex-direction: column; gap: 2px; }
6546
6625
 
@@ -9712,9 +9791,6 @@ body.setup-mode[data-theme="dark"] {
9712
9791
  border-radius: 12px;
9713
9792
  transition: border-color 0.15s ease;
9714
9793
  }
9715
- .billing-stat-card:hover {
9716
- border-color: color-mix(in srgb, var(--color-accent-primary) 40%, transparent);
9717
- }
9718
9794
  .billing-stat-primary {
9719
9795
  border-color: color-mix(in srgb, var(--color-accent-primary) 35%, transparent);
9720
9796
  }
@@ -9762,7 +9838,12 @@ body.setup-mode[data-theme="dark"] {
9762
9838
  }
9763
9839
 
9764
9840
  /* ── Heatmap ─────────────────────────────────────────────────────────── */
9765
- .billing-heatmap-row { width: 100%; }
9841
+ .billing-heatmap-row {
9842
+ display: grid;
9843
+ grid-template-columns: auto 1fr;
9844
+ gap: 1rem;
9845
+ width: 100%;
9846
+ }
9766
9847
  .billing-heat-dow-row {
9767
9848
  display: grid;
9768
9849
  grid-template-columns: repeat(7, 28px);
@@ -9790,11 +9871,11 @@ body.setup-mode[data-theme="dark"] {
9790
9871
  }
9791
9872
  .billing-heat-cell.is-empty { background: transparent; }
9792
9873
  .billing-heat-cell[data-level="0"] { background: var(--color-border-primary); }
9793
- .billing-heat-cell[data-level="1"] { background: #9be9a8; }
9794
- .billing-heat-cell[data-level="2"] { background: #40c463; }
9795
- .billing-heat-cell[data-level="3"] { background: #30a14e; }
9796
- .billing-heat-cell[data-level="4"] { background: #216e39; }
9797
- .billing-heat-cell[data-level="5"] { background: #0a4020; }
9874
+ .billing-heat-cell[data-level="1"] { background: color-mix(in srgb, var(--color-accent-primary) 20%, var(--color-bg-secondary)); }
9875
+ .billing-heat-cell[data-level="2"] { background: color-mix(in srgb, var(--color-accent-primary) 40%, var(--color-bg-secondary)); }
9876
+ .billing-heat-cell[data-level="3"] { background: color-mix(in srgb, var(--color-accent-primary) 60%, var(--color-bg-secondary)); }
9877
+ .billing-heat-cell[data-level="4"] { background: color-mix(in srgb, var(--color-accent-primary) 80%, var(--color-bg-secondary)); }
9878
+ .billing-heat-cell[data-level="5"] { background: var(--color-accent-primary); }
9798
9879
  .billing-heat-legend {
9799
9880
  display: flex;
9800
9881
  align-items: center;
@@ -9808,6 +9889,63 @@ body.setup-mode[data-theme="dark"] {
9808
9889
  aspect-ratio: auto;
9809
9890
  }
9810
9891
 
9892
+ /* ── Heatmap + Trend two-column row ──────────────────────────────────── */
9893
+ .billing-heatmap-card,
9894
+ .billing-trend-card {
9895
+ min-width: 0;
9896
+ min-height: 140px;
9897
+ }
9898
+
9899
+ /* ── Cost Trend Line Chart ───────────────────────────────────────────── */
9900
+ .billing-trend-total {
9901
+ font-size: 0.75rem;
9902
+ font-weight: 600;
9903
+ color: var(--color-accent-primary);
9904
+ }
9905
+ .billing-trend-chart {
9906
+ flex: 1;
9907
+ min-height: 0;
9908
+ }
9909
+ .billing-trend-svg {
9910
+ width: 100%;
9911
+ height: 100%;
9912
+ overflow: visible;
9913
+ }
9914
+ .billing-trend-grid-line {
9915
+ stroke: var(--color-border-primary);
9916
+ stroke-width: 1;
9917
+ }
9918
+ .billing-trend-y-label {
9919
+ font-size: 9px;
9920
+ fill: var(--color-text-secondary);
9921
+ text-anchor: end;
9922
+ }
9923
+ .billing-trend-x-label {
9924
+ font-size: 9px;
9925
+ fill: var(--color-text-secondary);
9926
+ text-anchor: middle;
9927
+ }
9928
+ .billing-trend-line {
9929
+ stroke: var(--color-accent-primary);
9930
+ stroke-width: 1.5;
9931
+ stroke-linecap: round;
9932
+ stroke-linejoin: round;
9933
+ }
9934
+ .billing-trend-area {
9935
+ fill: url(#billing-trend-grad);
9936
+ }
9937
+ .billing-trend-dot {
9938
+ fill: var(--color-accent-primary);
9939
+ stroke: var(--color-bg-secondary);
9940
+ stroke-width: 2;
9941
+ cursor: pointer;
9942
+ opacity: 0;
9943
+ transition: opacity 0.15s;
9944
+ }
9945
+ .billing-trend-svg:hover .billing-trend-dot {
9946
+ opacity: 1;
9947
+ }
9948
+
9811
9949
  /* ── Chart Card base ─────────────────────────────────────────────────── */
9812
9950
  .billing-chart-row { width: 100%; }
9813
9951
  .billing-chart-card {
@@ -10002,6 +10140,8 @@ body.setup-mode[data-theme="dark"] {
10002
10140
  border: 1px solid var(--color-border-primary);
10003
10141
  border-radius: 12px;
10004
10142
  padding: 1.25rem;
10143
+ height: 16rem;
10144
+ overflow-y: auto;
10005
10145
  }
10006
10146
  .billing-section h3 {
10007
10147
  font-size: 0.9375rem;
@@ -10103,6 +10243,7 @@ body.setup-mode[data-theme="dark"] {
10103
10243
  height: 100%;
10104
10244
  background: var(--color-accent-primary);
10105
10245
  border-radius: 3px;
10246
+ min-width: 8px;
10106
10247
  }
10107
10248
  .billing-model-cost {
10108
10249
  font-size: 0.8125rem;
@@ -10603,6 +10744,9 @@ body.setup-mode[data-theme="dark"] {
10603
10744
  text-align: right;
10604
10745
  color: var(--color-text-primary);
10605
10746
  }
10747
+ .billing-cell-total {
10748
+ color: var(--color-accent-primary);
10749
+ }
10606
10750
  .billing-cell-hit {
10607
10751
  color: #3b82f6;
10608
10752
  }
@@ -11180,6 +11324,7 @@ body.setup-mode[data-theme="dark"] {
11180
11324
  transform: translateX(-100%);
11181
11325
  margin-left: 0 !important;
11182
11326
  }
11327
+ #sidebar-resize-handle { display: none; }
11183
11328
 
11184
11329
  /* Overlay backdrop */
11185
11330
  #sidebar-overlay {
@@ -11219,7 +11364,9 @@ body.setup-mode[data-theme="dark"] {
11219
11364
 
11220
11365
  /* Session info bar: single-line, no hover-expand, font smaller */
11221
11366
  #session-info-bar {
11222
- padding: 0.1875rem 0.75rem;
11367
+ padding: 0.1875rem 0.5rem;
11368
+ margin-left: 0.75rem;
11369
+ margin-right: 0.75rem;
11223
11370
  font-size: 0.625rem;
11224
11371
  overflow: hidden;
11225
11372
  white-space: nowrap;
@@ -11246,9 +11393,14 @@ body.setup-mode[data-theme="dark"] {
11246
11393
  margin: 0 0.25rem;
11247
11394
  }
11248
11395
 
11396
+ /* Tighten input area outer padding on mobile */
11397
+ #input-area {
11398
+ padding: 0px 12px 12px;
11399
+ }
11400
+
11249
11401
  /* Input bar: proportional spacing, touch-friendly */
11250
11402
  #input-bar {
11251
- padding: 0.5rem 0.625rem;
11403
+ padding: 0.375rem 0.5rem;
11252
11404
  gap: 0.25rem;
11253
11405
  }
11254
11406
 
@@ -11264,12 +11416,12 @@ body.setup-mode[data-theme="dark"] {
11264
11416
  /* Textarea: prevent iOS auto-zoom (must be ≥1rem) */
11265
11417
  #user-input {
11266
11418
  font-size: 1rem;
11267
- padding: 0.4375rem 0.625rem;
11419
+ padding: 0.1875rem 0.375rem;
11268
11420
  }
11269
11421
 
11270
11422
  /* Send button: bigger tap target */
11271
11423
  #btn-send, #btn-interrupt {
11272
- padding: 0.4375rem 0.875rem;
11424
+ padding: 0.3125rem 0.75rem;
11273
11425
  font-size: 0.875rem;
11274
11426
  }
11275
11427
 
@@ -11329,7 +11481,7 @@ body.setup-mode[data-theme="dark"] {
11329
11481
  min-height: 0;
11330
11482
  }
11331
11483
  #skills-body {
11332
- padding: 1rem 1rem 5rem;
11484
+ padding: 1rem 1rem 1rem;
11333
11485
  gap: 0.75rem;
11334
11486
  overflow-y: auto;
11335
11487
  -webkit-overflow-scrolling: touch;
@@ -11430,9 +11582,11 @@ body.setup-mode[data-theme="dark"] {
11430
11582
  }
11431
11583
 
11432
11584
  /* ── Billing / Trash / Creator pages ── */
11433
- #billing-body { padding: 1rem 1rem 5rem; }
11434
- #trash-body { padding: 1rem 1rem 5rem; }
11435
- #creator-body { padding: 1rem 1rem 5rem; }
11585
+ #billing-body { padding: 1rem 1rem 1rem; }
11586
+ #trash-body { padding: 1rem 1rem 1rem; }
11587
+ #creator-body { padding: 1rem 1rem 1rem; }
11588
+
11589
+ .billing-heatmap-row { grid-template-columns: 1fr; }
11436
11590
 
11437
11591
  /* ── MCP page ── */
11438
11592
  #mcp-panel {
@@ -513,6 +513,57 @@ if ($("btn-toggle-sidebar")) {
513
513
  // Tap overlay to close sidebar on mobile
514
514
  $("sidebar-overlay").addEventListener("click", _closeSidebar);
515
515
 
516
+ // ── Sidebar resize ──────────────────────────────────────────────────────
517
+ (function _initSidebarResize() {
518
+ const sidebar = $("sidebar");
519
+ const handle = $("sidebar-resize-handle");
520
+ if (!sidebar || !handle) return;
521
+
522
+ const MIN_W = 12; // rem
523
+ const MAX_W = 32; // rem
524
+ const baseFontSize = parseFloat(getComputedStyle(document.documentElement).fontSize);
525
+
526
+ let startX = 0;
527
+ let startW = 0;
528
+
529
+ // Restore saved width
530
+ const saved = localStorage.getItem("sidebar-width");
531
+ if (saved) {
532
+ const w = parseFloat(saved);
533
+ if (w >= MIN_W && w <= MAX_W) {
534
+ sidebar.style.setProperty("--sidebar-width", w + "rem");
535
+ }
536
+ }
537
+
538
+ function _getWidth() {
539
+ return parseFloat(getComputedStyle(sidebar).getPropertyValue("--sidebar-width"));
540
+ }
541
+
542
+ handle.addEventListener("mousedown", (e) => {
543
+ e.preventDefault();
544
+ startX = e.clientX;
545
+ startW = _getWidth();
546
+ handle.classList.add("active");
547
+ document.body.style.cursor = "col-resize";
548
+ document.body.style.userSelect = "none";
549
+ });
550
+
551
+ document.addEventListener("mousemove", (e) => {
552
+ if (!handle.classList.contains("active")) return;
553
+ const dx = (e.clientX - startX) / baseFontSize;
554
+ const newW = Math.min(MAX_W, Math.max(MIN_W, startW + dx));
555
+ sidebar.style.setProperty("--sidebar-width", newW + "rem");
556
+ });
557
+
558
+ document.addEventListener("mouseup", () => {
559
+ if (!handle.classList.contains("active")) return;
560
+ handle.classList.remove("active");
561
+ document.body.style.cursor = "";
562
+ document.body.style.userSelect = "";
563
+ localStorage.setItem("sidebar-width", _getWidth());
564
+ });
565
+ })();
566
+
516
567
  // On mobile: start with sidebar hidden
517
568
  if (_isMobile()) _closeSidebar();
518
569