openclacky 1.3.3 → 1.3.5

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 (89) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +53 -0
  3. data/docs/rich_ui_guide.md +277 -0
  4. data/docs/rich_ui_refactor_plan.md +396 -0
  5. data/lib/clacky/agent/fake_tool_call_detector.rb +52 -0
  6. data/lib/clacky/agent/llm_caller.rb +10 -4
  7. data/lib/clacky/agent/session_serializer.rb +5 -3
  8. data/lib/clacky/agent/tool_executor.rb +0 -12
  9. data/lib/clacky/agent.rb +77 -11
  10. data/lib/clacky/agent_config.rb +2 -14
  11. data/lib/clacky/api_extension.rb +343 -0
  12. data/lib/clacky/api_extension_loader.rb +168 -0
  13. data/lib/clacky/cli.rb +93 -3
  14. data/lib/clacky/client.rb +47 -25
  15. data/lib/clacky/default_agents/_panels/git/panel.js +1 -1
  16. data/lib/clacky/default_agents/_panels/time_machine/panel.js +23 -1
  17. data/lib/clacky/default_agents/base_prompt.md +1 -0
  18. data/lib/clacky/default_extensions/meeting/handler.rb +331 -0
  19. data/lib/clacky/default_extensions/meeting/meeting.js +790 -0
  20. data/lib/clacky/default_extensions/meeting/meta.yml +3 -0
  21. data/lib/clacky/default_extensions/meeting/skills/meeting-summarizer/SKILL.md +44 -0
  22. data/lib/clacky/default_skills/media-gen/SKILL.md +72 -6
  23. data/lib/clacky/default_skills/media-gen/scripts/video_seq.sh +114 -0
  24. data/lib/clacky/idle_compression_timer.rb +3 -1
  25. data/lib/clacky/json_ui_controller.rb +1 -1
  26. data/lib/clacky/locales/en.rb +26 -0
  27. data/lib/clacky/locales/i18n.rb +26 -0
  28. data/lib/clacky/locales/zh.rb +26 -0
  29. data/lib/clacky/media/base.rb +60 -0
  30. data/lib/clacky/media/dashscope.rb +385 -21
  31. data/lib/clacky/media/gemini.rb +9 -0
  32. data/lib/clacky/media/generator.rb +52 -0
  33. data/lib/clacky/media/openai_compat.rb +166 -0
  34. data/lib/clacky/null_ui_controller.rb +13 -0
  35. data/lib/clacky/plain_ui_controller.rb +1 -1
  36. data/lib/clacky/providers.rb +50 -2
  37. data/lib/clacky/rich_ui/components/base_component.rb +50 -0
  38. data/lib/clacky/rich_ui/components/dialogs/approval_dialog.rb +142 -0
  39. data/lib/clacky/rich_ui/components/dialogs/config_menu_dialog.rb +106 -0
  40. data/lib/clacky/rich_ui/components/dialogs/form_dialog.rb +128 -0
  41. data/lib/clacky/rich_ui/components/sidebar.rb +119 -0
  42. data/lib/clacky/rich_ui/components/sidebar_panels.rb +134 -0
  43. data/lib/clacky/rich_ui/components/status_view.rb +58 -0
  44. data/lib/clacky/rich_ui/components/thinking_live_view.rb +79 -0
  45. data/lib/clacky/rich_ui/entry_tracker.rb +56 -0
  46. data/lib/clacky/rich_ui/layout_adapter.rb +16 -0
  47. data/lib/clacky/rich_ui/progress_handle_adapter.rb +24 -0
  48. data/lib/clacky/rich_ui/rich_ui_controller.rb +868 -0
  49. data/lib/clacky/rich_ui/shell/rich_agent_shell.rb +184 -0
  50. data/lib/clacky/rich_ui/view_renderer.rb +291 -0
  51. data/lib/clacky/rich_ui.rb +57 -0
  52. data/lib/clacky/rich_ui_controller.rb +3 -1549
  53. data/lib/clacky/server/api_extension_dispatcher.rb +120 -0
  54. data/lib/clacky/server/channel/channel_ui_controller.rb +1 -1
  55. data/lib/clacky/server/http_server.rb +291 -109
  56. data/lib/clacky/server/session_registry.rb +5 -3
  57. data/lib/clacky/server/web_ui_controller.rb +3 -2
  58. data/lib/clacky/shell_hook_loader.rb +1 -1
  59. data/lib/clacky/skill_loader.rb +14 -2
  60. data/lib/clacky/tools/edit.rb +14 -2
  61. data/lib/clacky/tools/terminal/output_cleaner.rb +1 -3
  62. data/lib/clacky/tools/terminal.rb +0 -43
  63. data/lib/clacky/ui2/components/modal_component.rb +1 -1
  64. data/lib/clacky/ui2/ui_controller.rb +147 -31
  65. data/lib/clacky/ui_interface.rb +10 -1
  66. data/lib/clacky/utils/encoding.rb +25 -0
  67. data/lib/clacky/version.rb +1 -1
  68. data/lib/clacky/web/app.css +201 -81
  69. data/lib/clacky/web/app.js +65 -7
  70. data/lib/clacky/web/components/onboard.js +19 -16
  71. data/lib/clacky/web/core/aside.js +8 -3
  72. data/lib/clacky/web/core/ext.js +1 -1
  73. data/lib/clacky/web/features/brand/view.js +8 -5
  74. data/lib/clacky/web/features/channels/store.js +1 -20
  75. data/lib/clacky/web/features/mcp/store.js +1 -20
  76. data/lib/clacky/web/features/profile/store.js +1 -13
  77. data/lib/clacky/web/features/profile/view.js +16 -4
  78. data/lib/clacky/web/features/skills/store.js +36 -23
  79. data/lib/clacky/web/features/skills/view.js +32 -1
  80. data/lib/clacky/web/features/version/store.js +2 -0
  81. data/lib/clacky/web/features/workspace/view.js +1 -1
  82. data/lib/clacky/web/i18n.js +48 -13
  83. data/lib/clacky/web/index.html +24 -17
  84. data/lib/clacky/web/sessions.js +427 -79
  85. data/lib/clacky/web/settings.js +143 -113
  86. data/lib/clacky/web/ws-dispatcher.js +18 -6
  87. data/lib/clacky.rb +27 -5
  88. metadata +45 -2
  89. data/lib/clacky/media/output_dir.rb +0 -43
@@ -526,7 +526,7 @@ body {
526
526
  /* The sidebar sits on the outer frame (bg-primary) so it's visibly a layer
527
527
  BEHIND the chat surface (bg-secondary). Subtle right border separates them. */
528
528
  #sidebar {
529
- --sidebar-width: 15rem;
529
+ --sidebar-width: 16rem;
530
530
  width: var(--sidebar-width);
531
531
  min-width: var(--sidebar-width);
532
532
  background: var(--color-bg-primary);
@@ -650,38 +650,38 @@ body {
650
650
  position: relative;
651
651
  display: flex;
652
652
  align-items: center;
653
- border-radius: 5px;
653
+ border-radius: 6px;
654
654
  overflow: visible;
655
655
  }
656
656
  .btn-split-main {
657
- height: 1.375rem;
658
- padding: 0 0.5rem;
657
+ height: 1.5rem;
658
+ padding: 0 0.625rem;
659
659
  border: none;
660
- border-radius: 5px 0 0 5px;
660
+ border-radius: 6px 0 0 6px;
661
661
  background: var(--color-accent-primary);
662
662
  color: #fff;
663
- font-size: 0.6875rem;
663
+ font-size: 0.75rem;
664
664
  font-weight: 500;
665
665
  white-space: nowrap;
666
666
  line-height: 1;
667
667
  cursor: pointer;
668
- transition: background 0.15s, color 0.15s, box-shadow 0.15s;
668
+ transition: background 0.15s, box-shadow 0.15s;
669
669
  }
670
670
  .btn-split-main:hover {
671
671
  background: var(--color-button-primary-hover);
672
- box-shadow: 0 2px 4px rgba(0,0,0,0.1);
672
+ box-shadow: 0 2px 6px rgba(0,0,0,0.15);
673
673
  }
674
674
  .btn-split-arrow {
675
- height: 1.375rem;
676
- padding: 0 0.25rem;
675
+ height: 1.5rem;
676
+ padding: 0 0.35rem;
677
677
  border: none;
678
678
  border-left: 1px solid rgba(255,255,255,0.3);
679
- border-radius: 0 5px 5px 0;
679
+ border-radius: 0 6px 6px 0;
680
680
  background: var(--color-accent-primary);
681
681
  color: #fff;
682
- font-size: 0.625rem;
682
+ font-size: 0.75rem;
683
683
  cursor: pointer;
684
- transition: background 0.15s, color 0.15s;
684
+ transition: background 0.15s;
685
685
  }
686
686
  .btn-split-arrow:hover {
687
687
  background: var(--color-button-primary-hover);
@@ -690,7 +690,7 @@ body {
690
690
  /* ── Dropdown menu ───────────────────────────────────────────────────────── */
691
691
  .new-session-dropdown {
692
692
  position: absolute;
693
- top: calc(100% + 0.25rem);
693
+ top: 100%;
694
694
  right: 0;
695
695
  min-width: 11.25rem;
696
696
  background: var(--color-bg-card);
@@ -698,13 +698,13 @@ body {
698
698
  border-radius: 7px;
699
699
  box-shadow: var(--shadow-md);
700
700
  z-index: 200;
701
- padding: 0.25rem 0;
701
+ padding: 0.375rem 0 0.25rem;
702
702
  overflow: hidden;
703
703
  }
704
704
  .new-session-dropdown[hidden] { display: none; }
705
705
  .dropdown-item {
706
- padding: 0.25rem 0.625rem;
707
- font-size: 0.6875rem;
706
+ padding: 0.4rem 0.875rem;
707
+ font-size: 0.8125rem;
708
708
  color: var(--color-text-secondary);
709
709
  cursor: pointer;
710
710
  white-space: nowrap;
@@ -1799,7 +1799,7 @@ body {
1799
1799
  white-space: nowrap;
1800
1800
  transition: background .12s;
1801
1801
  }
1802
- .task-run-btn:hover { background: var(--color-accent-hover); }
1802
+ .task-run-btn:hover { background: var(--color-button-primary-hover); }
1803
1803
  .task-action-btn {
1804
1804
  display: inline-flex;
1805
1805
  align-items: center;
@@ -1938,7 +1938,7 @@ body {
1938
1938
  the slot has no panels for the current agent it stays empty and the whole
1939
1939
  column collapses to zero width. */
1940
1940
  #session-aside {
1941
- --session-aside-width: 360px;
1941
+ --session-aside-width: 16rem;
1942
1942
  width: var(--session-aside-width);
1943
1943
  flex-shrink: 0;
1944
1944
  position: relative;
@@ -1976,7 +1976,7 @@ body {
1976
1976
  transition: background-color var(--transition-base);
1977
1977
  }
1978
1978
  .session-aside-resize:hover::after,
1979
- .session-aside-resize.active::after { background: var(--color-accent-primary); }
1979
+ .session-aside-resize.active::after { background: transparent; }
1980
1980
 
1981
1981
  .session-aside-collapse {
1982
1982
  position: absolute;
@@ -2271,13 +2271,16 @@ body {
2271
2271
  /* Sit above the next message in case text would otherwise clip beneath it. */
2272
2272
  z-index: 1;
2273
2273
  }
2274
- .msg:hover .msg-time {
2274
+ .msg:hover .msg-time,
2275
+ .msg-user-wrap:hover .msg-time {
2275
2276
  opacity: 1;
2276
2277
  transform: translateY(0);
2277
2278
  }
2278
2279
  /* Time color / alignment: anchor to the bubble's own side, let width be
2279
2280
  driven by content — prevents overflow on narrow bubbles. */
2280
- .msg-user .msg-time { display: none; }
2281
+ /* User timestamp: appended to .msg-user-wrap (not bubble), sits below action
2282
+ buttons, right-aligned. */
2283
+ .msg-user-wrap > .msg-time { color: var(--color-text-secondary); top: auto; bottom: 0.2rem; right: 0; left: auto; padding-right: 0.25rem; }
2281
2284
  .msg-assistant .msg-time { color: var(--color-text-secondary); left: 0; right: auto; padding-left: 0.25rem; }
2282
2285
 
2283
2286
  .msg-user { background: var(--color-accent-soft); color: var(--color-text-primary); white-space: pre-wrap; }
@@ -2289,11 +2292,11 @@ body {
2289
2292
  position: relative;
2290
2293
  display: flex;
2291
2294
  justify-content: flex-end;
2292
- padding-bottom: 1.8rem;
2295
+ padding-bottom: 3rem;
2293
2296
  }
2294
2297
  .msg-user-actions {
2295
2298
  position: absolute;
2296
- top: calc(100% - 1.5rem + 2px);
2299
+ bottom: 1.1rem;
2297
2300
  right: 0;
2298
2301
  display: flex;
2299
2302
  gap: 0.125rem;
@@ -2305,6 +2308,7 @@ body {
2305
2308
  .msg-user-wrap:hover .msg-user-actions { opacity: 1; pointer-events: auto; }
2306
2309
  .msg-user-wrap:has(.msg-user.editing) { padding-bottom: 0; }
2307
2310
  .msg-user-wrap:has(.msg-user.editing) .msg-user-actions { display: none; }
2311
+ .msg-user-wrap:has(.msg-user.editing) .msg-time { display: none; }
2308
2312
 
2309
2313
  .msg-user-action-btn {
2310
2314
  display: inline-flex;
@@ -2452,6 +2456,36 @@ body {
2452
2456
  .msg-copy-btn { opacity: 0.65; }
2453
2457
  }
2454
2458
 
2459
+ /* ── Inline "start" button on unchecked task-list items ──────────────────── */
2460
+ .msg-assistant li.task-list-item { position: relative; }
2461
+ .msg-todo-spawn {
2462
+ display: inline-flex;
2463
+ align-items: center;
2464
+ margin-left: 0.5rem;
2465
+ padding: 0.05rem 0.5rem;
2466
+ font-size: 0.75rem;
2467
+ line-height: 1.4;
2468
+ border: 1px solid var(--color-border-primary);
2469
+ border-radius: 5px;
2470
+ background: var(--color-bg-primary);
2471
+ color: var(--color-text-secondary);
2472
+ cursor: pointer;
2473
+ opacity: 0;
2474
+ vertical-align: middle;
2475
+ transition: opacity 0.15s ease, color 0.15s ease, background 0.15s ease, border-color 0.15s ease;
2476
+ }
2477
+ .msg-assistant:hover .msg-todo-spawn,
2478
+ .msg-todo-spawn:focus-visible { opacity: 1; }
2479
+ .msg-todo-spawn:hover {
2480
+ color: var(--color-text-primary);
2481
+ background: var(--color-bg-tertiary);
2482
+ border-color: var(--color-border-secondary);
2483
+ }
2484
+ .msg-todo-spawn:disabled { opacity: 0.5; cursor: default; }
2485
+ @media (hover: none) {
2486
+ .msg-todo-spawn { opacity: 0.7; }
2487
+ }
2488
+
2455
2489
  /* ── Markdown rendering inside assistant messages ────────────────────────── */
2456
2490
  .msg-assistant p:last-child { margin-bottom: 0; }
2457
2491
  .msg-assistant h1, .msg-assistant h2, .msg-assistant h3,
@@ -2686,7 +2720,7 @@ body {
2686
2720
  align-self: flex-start;
2687
2721
  display: flex;
2688
2722
  flex-direction: column;
2689
- gap: 0.75rem;
2723
+ gap: 0.5rem;
2690
2724
  }
2691
2725
  .msg-error .retry-btn {
2692
2726
  align-self: flex-start;
@@ -2717,6 +2751,56 @@ body {
2717
2751
  .msg-error a:hover {
2718
2752
  opacity: 0.8;
2719
2753
  }
2754
+ .error-raw-detail {
2755
+ margin-top: -0.125rem;
2756
+ font-size: 0.75rem;
2757
+ align-self: flex-start;
2758
+ max-width: 100%;
2759
+ }
2760
+ .error-raw-detail summary {
2761
+ cursor: pointer;
2762
+ font-size: 0.7rem;
2763
+ font-weight: 500;
2764
+ user-select: none;
2765
+ color: var(--color-error);
2766
+ opacity: 0.6;
2767
+ list-style: none;
2768
+ display: inline-flex;
2769
+ align-items: center;
2770
+ gap: 0.25rem;
2771
+ transition: opacity 0.15s;
2772
+ }
2773
+ .error-raw-detail summary::-webkit-details-marker { display: none; }
2774
+ .error-raw-detail summary::before {
2775
+ content: "";
2776
+ display: inline-block;
2777
+ width: 0;
2778
+ height: 0;
2779
+ border-top: 4px solid transparent;
2780
+ border-bottom: 4px solid transparent;
2781
+ border-left: 5px solid currentColor;
2782
+ transition: transform 0.15s;
2783
+ flex-shrink: 0;
2784
+ }
2785
+ .error-raw-detail[open] summary::before {
2786
+ transform: rotate(90deg);
2787
+ }
2788
+ .error-raw-detail summary:hover {
2789
+ opacity: 0.9;
2790
+ }
2791
+ .error-raw-detail pre {
2792
+ margin: 0.375rem 0 0;
2793
+ padding: 0.5rem 0.625rem;
2794
+ background: rgba(255, 59, 48, 0.05);
2795
+ border: 1px solid rgba(255, 59, 48, 0.12);
2796
+ border-radius: 5px;
2797
+ font-family: var(--font-mono, monospace);
2798
+ font-size: 0.7rem;
2799
+ line-height: 1.5;
2800
+ white-space: pre-wrap;
2801
+ word-break: break-all;
2802
+ color: var(--color-text-secondary);
2803
+ }
2720
2804
  .msg-success { color: var(--color-success); align-self: flex-start; font-size: 0.8125rem; }
2721
2805
  .tool-name { color: var(--color-warning); font-weight: 600; }
2722
2806
  .progress-msg { color: var(--color-accent-primary); font-size: 0.75rem; align-self: center; }
@@ -4583,7 +4667,6 @@ body {
4583
4667
  font-size: 0.8125rem;
4584
4668
  color: var(--color-text-primary);
4585
4669
  cursor: pointer;
4586
- margin-top: 0.5rem;
4587
4670
  }
4588
4671
  .model-field-checkbox input[type="checkbox"] {
4589
4672
  width: 15px;
@@ -4780,7 +4863,7 @@ body {
4780
4863
  /* ── Model Cards Grid Container ─────────────────────────────────────────────── */
4781
4864
  #model-cards {
4782
4865
  display: grid;
4783
- grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
4866
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
4784
4867
  gap: 1rem;
4785
4868
  }
4786
4869
 
@@ -6698,24 +6781,14 @@ body {
6698
6781
  white-space: nowrap;
6699
6782
  }
6700
6783
  .brand-skill-version.installed {
6701
- background: #1f3a5f;
6702
- color: #7eb8f7;
6703
- border: 1px solid #1d4070;
6784
+ background: color-mix(in srgb, var(--color-accent-primary) 15%, transparent);
6785
+ color: var(--color-accent-primary);
6786
+ border: 1px solid color-mix(in srgb, var(--color-accent-primary) 35%, transparent);
6704
6787
  }
6705
6788
  .brand-skill-version.latest {
6706
- background: #1f4e9e;
6707
- color: #bfdbfe;
6708
- border: 1px solid #1d4070;
6709
- }
6710
- [data-theme="light"] .brand-skill-version.installed {
6711
- background: #eff6ff;
6712
- color: #1d4ed8;
6713
- border: 1px solid #bfdbfe;
6714
- }
6715
- [data-theme="light"] .brand-skill-version.latest {
6716
- background: #1d4ed8;
6789
+ background: var(--color-accent-primary);
6717
6790
  color: #ffffff;
6718
- border: 1px solid #1e40af;
6791
+ border: 1px solid var(--color-accent-primary);
6719
6792
  }
6720
6793
  .brand-skill-update-arrow {
6721
6794
  color: var(--color-text-secondary);
@@ -6795,17 +6868,11 @@ body {
6795
6868
  border-color: var(--color-accent-primary);
6796
6869
  }
6797
6870
  .btn-brand-update {
6798
- background: #1f4e9e;
6799
- color: #bfdbfe;
6800
- border: 1px solid #1d4070;
6801
- }
6802
- .btn-brand-update:hover { background: #1a3f85; }
6803
- [data-theme="light"] .btn-brand-update {
6804
- background: #1d4ed8;
6871
+ background: var(--color-accent-primary);
6805
6872
  color: #ffffff;
6806
- border: 1px solid #1e40af;
6873
+ border: 1px solid var(--color-accent-primary);
6807
6874
  }
6808
- [data-theme="light"] .btn-brand-update:hover { background: #1e40af; }
6875
+ .btn-brand-update:hover { background: var(--color-accent-hover); }
6809
6876
  .btn-brand-install:disabled,
6810
6877
  .btn-brand-update:disabled {
6811
6878
  opacity: 0.5;
@@ -6846,6 +6913,22 @@ body {
6846
6913
  border-color: var(--color-error-border, #f5c6c6);
6847
6914
  background: var(--color-error-bg, #fff0f0);
6848
6915
  }
6916
+ .btn-skill-edit {
6917
+ background: transparent;
6918
+ border: 1px solid transparent;
6919
+ border-radius: 6px;
6920
+ color: var(--color-text-tertiary);
6921
+ cursor: pointer;
6922
+ padding: 0.25rem;
6923
+ display: inline-flex;
6924
+ align-items: center;
6925
+ transition: color .15s, border-color .15s, background .15s;
6926
+ }
6927
+ .btn-skill-edit:hover {
6928
+ color: var(--color-text-primary);
6929
+ border-color: var(--color-border-primary);
6930
+ background: var(--color-bg-hover);
6931
+ }
6849
6932
 
6850
6933
 
6851
6934
  /* ── CodeEditor (CodeMirror 6) Modal ────────────────────────────────────── */
@@ -6926,10 +7009,10 @@ body {
6926
7009
  background: var(--color-bg-hover);
6927
7010
  }
6928
7011
  .code-editor-body .cm-activeLine {
6929
- background: var(--color-bg-hover);
7012
+ background: color-mix(in srgb, var(--color-bg-hover) 60%, transparent);
6930
7013
  }
6931
7014
  .code-editor-body .cm-selectionBackground {
6932
- background: color-mix(in srgb, var(--color-accent-primary) 20%, transparent) !important;
7015
+ background: color-mix(in srgb, var(--color-accent-primary) 40%, transparent) !important;
6933
7016
  }
6934
7017
  .code-editor-footer {
6935
7018
  display: flex;
@@ -7396,28 +7479,35 @@ body.setup-mode[data-theme="dark"] {
7396
7479
  transform: translateY(1px);
7397
7480
  }
7398
7481
  #setup-device-error {
7399
- margin-top: 0.5rem;
7482
+ margin-top: 0;
7483
+ min-height: 0;
7400
7484
  }
7401
7485
  #setup-device-error.result-fail { color: var(--color-error); }
7402
7486
 
7403
7487
  /* Manual-config collapsible (secondary path) */
7404
- #setup-manual-details {
7405
- margin-top: 0.25rem;
7406
- }
7407
- #setup-manual-summary {
7488
+ #setup-manual-toggle {
7489
+ display: block;
7490
+ width: 100%;
7491
+ margin-top: 0.5rem;
7492
+ padding: 0.5rem 0;
7493
+ background: none;
7494
+ border: none;
7408
7495
  cursor: pointer;
7409
- font-size: 0.75rem;
7496
+ font-size: 0.8rem;
7410
7497
  color: var(--color-text-secondary);
7411
7498
  text-align: center;
7412
- list-style: none;
7413
- padding: 0.5rem 0;
7499
+ text-decoration: underline;
7500
+ text-underline-offset: 3px;
7414
7501
  user-select: none;
7502
+ transition: color 0.15s;
7415
7503
  }
7416
- #setup-manual-summary::-webkit-details-marker { display: none; }
7417
- #setup-manual-summary:hover { color: var(--color-text-primary); }
7418
- #setup-manual-details[open] #setup-manual-summary {
7419
- margin-bottom: 0.5rem;
7420
- border-bottom: 1px solid var(--color-border-primary);
7504
+ #setup-manual-toggle:hover {
7505
+ color: var(--color-text-primary);
7506
+ }
7507
+ #setup-manual-section {
7508
+ margin-top: 0.75rem;
7509
+ padding-top: 0.75rem;
7510
+ border-top: 1px solid var(--color-border-primary);
7421
7511
  }
7422
7512
 
7423
7513
  /* Bottom action row: [← Back] [Test & Continue →] */
@@ -10036,7 +10126,7 @@ body.setup-mode[data-theme="dark"] {
10036
10126
  border-right: 1px solid var(--color-border-primary);
10037
10127
  }
10038
10128
  .billing-period-btn:hover {
10039
- background: var(--color-bg-tertiary);
10129
+ background: var(--color-bg-hover);
10040
10130
  color: var(--color-text-primary);
10041
10131
  }
10042
10132
  .billing-period-btn.active {
@@ -10896,7 +10986,7 @@ body.setup-mode[data-theme="dark"] {
10896
10986
  white-space: nowrap;
10897
10987
  transition: opacity 0.15s ease, transform 0.05s ease;
10898
10988
  }
10899
- .btn-mcp-cta:hover { opacity: 0.85; }
10989
+ .btn-mcp-cta:hover { background: var(--color-button-primary-hover) }
10900
10990
  .btn-mcp-cta:active { transform: scale(0.98); }
10901
10991
  .btn-mcp-cta-large {
10902
10992
  padding: 0.625rem 1.125rem;
@@ -11009,6 +11099,49 @@ body.setup-mode[data-theme="dark"] {
11009
11099
  color: var(--color-error, #d33);
11010
11100
  }
11011
11101
 
11102
+ /* ── Accent Color Swatches ───────────────────────────────────────────── */
11103
+ .settings-accent-swatches {
11104
+ display: flex;
11105
+ flex-wrap: wrap;
11106
+ gap: 0.5rem;
11107
+ padding: 0.25rem 0;
11108
+ }
11109
+
11110
+ .settings-accent-swatch {
11111
+ width: 2.25rem;
11112
+ height: 2.25rem;
11113
+ border-radius: 50%;
11114
+ border: none;
11115
+ cursor: pointer;
11116
+ padding: 0;
11117
+ transition: transform 0.15s;
11118
+ outline: none;
11119
+ position: relative;
11120
+ }
11121
+
11122
+ .settings-accent-swatch:hover {
11123
+ transform: scale(1.15);
11124
+ }
11125
+
11126
+ .settings-accent-swatch.active::after {
11127
+ content: "";
11128
+ position: absolute;
11129
+ width: 0.375rem;
11130
+ height: 0.625rem;
11131
+ border-right: 0.125rem solid white;
11132
+ border-bottom: 0.125rem solid white;
11133
+ top: 46%;
11134
+ left: 50%;
11135
+ transform: translate(-50%, -50%) rotate(45deg);
11136
+ }
11137
+
11138
+ .swatch-indigo { background: #4f46e5; }
11139
+ .swatch-aurora-blue { background: #3B82F6; }
11140
+ .swatch-forest-green { background: #10B981; }
11141
+ .swatch-sunrise-orange { background: #F59E0B; }
11142
+ .swatch-rose-violet { background: #8B5CF6; }
11143
+ .swatch-coral-red { background: #EF4444; }
11144
+
11012
11145
  /* ── Sessions List ───────────────────────────────────────────────────── */
11013
11146
  .billing-sessions-row {
11014
11147
  margin-top: 1rem;
@@ -12113,16 +12246,3 @@ body.setup-mode[data-theme="dark"] {
12113
12246
  }
12114
12247
  }
12115
12248
 
12116
- /* ════ Media output directory subsection (nested under #media-section) ════ */
12117
- #media-output-dir-section.settings-subsection {
12118
- display: flex;
12119
- flex-direction: column;
12120
- gap: 0.5rem;
12121
- max-width: 48rem;
12122
- }
12123
- #media-output-dir-section .settings-subsection-desc {
12124
- font-size: 0.8125rem;
12125
- color: var(--color-text-secondary);
12126
- line-height: 1.5;
12127
- margin: 0;
12128
- }
@@ -10,6 +10,14 @@
10
10
  // ── DOM helper (shared by all modules loaded after this) ──────────────────
11
11
  const $ = id => document.getElementById(id);
12
12
 
13
+ // ── Inject X-Lang header into every fetch request ─────────────────────────
14
+ const _nativeFetch = window.fetch;
15
+ window.fetch = function(input, init = {}) {
16
+ const headers = new Headers(init.headers || {});
17
+ if (!headers.has("X-Lang")) headers.set("X-Lang", I18n.lang());
18
+ return _nativeFetch.call(this, input, { ...init, headers });
19
+ };
20
+
13
21
  // ── Utilities (shared) ────────────────────────────────────────────────────
14
22
  function escapeHtml(str) {
15
23
  return String(str)
@@ -325,13 +333,53 @@ const Modal = (() => {
325
333
  /** Show a yes/no confirmation dialog. Returns a Promise<boolean>. */
326
334
  function confirm(message) {
327
335
  return new Promise(resolve => {
336
+ const overlay = $("modal-overlay");
337
+ $("modal-message").textContent = message;
338
+ $("modal-skip-label").style.display = "none";
339
+ $("modal-skip-cb").checked = false;
340
+ if (overlay.parentNode !== document.body || overlay.nextSibling) {
341
+ document.body.appendChild(overlay);
342
+ }
343
+ overlay.style.display = "flex";
344
+
345
+ const cleanup = (result) => {
346
+ overlay.style.display = "none";
347
+ $("modal-yes").onclick = null;
348
+ $("modal-no").onclick = null;
349
+ resolve(result);
350
+ };
351
+ $("modal-yes").onclick = () => cleanup(true);
352
+ $("modal-no").onclick = () => cleanup(false);
353
+ });
354
+ }
355
+
356
+ /**
357
+ * Like confirm(), but shows a "don't show again" checkbox.
358
+ * If localStorage[storageKey] === "1", resolves true immediately.
359
+ * Returns Promise<boolean>.
360
+ */
361
+ function confirmOnce(storageKey, message, skipLabel) {
362
+ if (localStorage.getItem(storageKey) === "1") return Promise.resolve(true);
363
+
364
+ return new Promise(resolve => {
365
+ const overlay = $("modal-overlay");
328
366
  $("modal-message").textContent = message;
329
- $("modal-overlay").style.display = "flex";
367
+ $("modal-skip-text").textContent = skipLabel;
368
+ $("modal-skip-cb").checked = false;
369
+ $("modal-skip-label").style.display = "flex";
370
+ if (overlay.parentNode !== document.body || overlay.nextSibling) {
371
+ document.body.appendChild(overlay);
372
+ }
373
+ overlay.style.display = "flex";
330
374
 
331
375
  const cleanup = (result) => {
332
- $("modal-overlay").style.display = "none";
376
+ overlay.style.display = "none";
377
+ $("modal-skip-label").style.display = "none";
333
378
  $("modal-yes").onclick = null;
334
379
  $("modal-no").onclick = null;
380
+ if (result && $("modal-skip-cb").checked) {
381
+ localStorage.setItem(storageKey, "1");
382
+ }
335
383
  resolve(result);
336
384
  };
337
385
  $("modal-yes").onclick = () => cleanup(true);
@@ -342,10 +390,15 @@ const Modal = (() => {
342
390
  /** Show a text input prompt dialog. Returns a Promise<string|null>. */
343
391
  function prompt(message, defaultValue = "") {
344
392
  return new Promise(resolve => {
393
+ const overlay = $("prompt-modal-overlay");
345
394
  $("prompt-modal-message").textContent = message;
346
395
  const input = $("prompt-modal-input");
347
396
  input.value = defaultValue;
348
- $("prompt-modal-overlay").style.display = "flex";
397
+ // Re-attach to <body> end so it stacks above dynamically-appended overlays.
398
+ if (overlay.parentNode !== document.body || overlay.nextSibling) {
399
+ document.body.appendChild(overlay);
400
+ }
401
+ overlay.style.display = "flex";
349
402
 
350
403
  // Auto-focus and select all text
351
404
  setTimeout(() => {
@@ -375,10 +428,15 @@ const Modal = (() => {
375
428
  /** Show a rename dialog. Returns a Promise<string|null>. */
376
429
  function rename(currentName = "") {
377
430
  return new Promise(resolve => {
431
+ const overlay = $("rename-modal-overlay");
378
432
  const input = $("rename-modal-input");
379
433
  input.value = currentName;
380
434
  input.classList.remove("input-error");
381
- $("rename-modal-overlay").style.display = "flex";
435
+ // Re-attach to <body> end so it stacks above dynamically-appended overlays.
436
+ if (overlay.parentNode !== document.body || overlay.nextSibling) {
437
+ document.body.appendChild(overlay);
438
+ }
439
+ overlay.style.display = "flex";
382
440
 
383
441
  setTimeout(() => {
384
442
  input.focus();
@@ -423,7 +481,7 @@ const Modal = (() => {
423
481
  });
424
482
  }
425
483
 
426
- return { confirm, prompt, rename };
484
+ return { confirm, confirmOnce, prompt, rename };
427
485
  })();
428
486
 
429
487
  // ── Toast helper ──────────────────────────────────────────────────────────
@@ -550,7 +608,7 @@ $("sidebar-overlay").addEventListener("click", _closeSidebar);
550
608
  let startW = 0;
551
609
 
552
610
  // Restore saved width
553
- const saved = localStorage.getItem("sidebar-width");
611
+ const saved = localStorage.getItem("clacky-sidebar-width");
554
612
  if (saved) {
555
613
  const w = parseFloat(saved);
556
614
  if (w >= MIN_W && w <= MAX_W) {
@@ -583,7 +641,7 @@ $("sidebar-overlay").addEventListener("click", _closeSidebar);
583
641
  handle.classList.remove("active");
584
642
  document.body.style.cursor = "";
585
643
  document.body.style.userSelect = "";
586
- localStorage.setItem("sidebar-width", _getWidth());
644
+ localStorage.setItem("clacky-sidebar-width", _getWidth());
587
645
  });
588
646
  })();
589
647
 
@@ -110,6 +110,11 @@ const Onboard = (() => {
110
110
  $("setup-phase-key").style.display = step === "key" ? "" : "none";
111
111
  $("setup-dot-1").className = "setup-step" + (step === "lang" ? " active" : " done");
112
112
  $("setup-dot-2").className = "setup-step" + (step === "key" ? " active" : "");
113
+ if (step === "key") {
114
+ $("setup-device-block").style.display = "";
115
+ $("setup-manual-toggle").style.display = "";
116
+ $("setup-manual-section").style.display = "none";
117
+ }
113
118
  }
114
119
 
115
120
  // Step 2 — API key setup
@@ -367,9 +372,20 @@ const Onboard = (() => {
367
372
 
368
373
  $("setup-btn-test").addEventListener("click", _testAndSave);
369
374
 
370
- // Back to Step 1
375
+ $("setup-manual-toggle").addEventListener("click", () => {
376
+ $("setup-device-block").style.display = "none";
377
+ $("setup-manual-toggle").style.display = "none";
378
+ $("setup-manual-section").style.display = "";
379
+ });
380
+
371
381
  $("setup-btn-back").addEventListener("click", () => {
372
- _showSetupStep("lang");
382
+ if ($("setup-manual-section").style.display !== "none") {
383
+ $("setup-device-block").style.display = "";
384
+ $("setup-manual-toggle").style.display = "";
385
+ $("setup-manual-section").style.display = "none";
386
+ } else {
387
+ _showSetupStep("lang");
388
+ }
373
389
  });
374
390
 
375
391
  _bindDeviceStep();
@@ -559,20 +575,7 @@ const Onboard = (() => {
559
575
  async function _launchOnboardSession() {
560
576
  try {
561
577
  await _complete();
562
- const res = await fetch("/api/sessions", {
563
- method: "POST",
564
- headers: { "Content-Type": "application/json" },
565
- body: JSON.stringify({ name: "Onboard", source: "setup" })
566
- });
567
- const data = await res.json();
568
- const session = data.session;
569
- if (!session) throw new Error("No session returned");
570
-
571
- Sessions.add(session);
572
- Sessions.renderList();
573
- Sessions.setPendingMessage(session.id, `/onboard lang:${_selectedLang}`);
574
- Sessions.select(session.id);
575
-
578
+ await Sessions.startWith(`/onboard lang:${_selectedLang}`, { name: "Onboard", source: "setup" });
576
579
  _bootUI();
577
580
  } catch (_) {
578
581
  // Fallback: just boot normally if session creation fails