openclacky 1.3.4 → 1.3.6

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 (65) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +39 -0
  3. data/lib/clacky/agent/fake_tool_call_detector.rb +52 -0
  4. data/lib/clacky/agent/message_compressor.rb +32 -8
  5. data/lib/clacky/agent/message_compressor_helper.rb +113 -12
  6. data/lib/clacky/agent/session_serializer.rb +3 -2
  7. data/lib/clacky/agent/tool_executor.rb +0 -12
  8. data/lib/clacky/agent.rb +74 -12
  9. data/lib/clacky/api_extension.rb +81 -0
  10. data/lib/clacky/api_extension_loader.rb +13 -1
  11. data/lib/clacky/cli.rb +0 -1
  12. data/lib/clacky/client.rb +14 -17
  13. data/lib/clacky/default_agents/_panels/time_machine/panel.js +22 -0
  14. data/lib/clacky/default_agents/base_prompt.md +1 -0
  15. data/lib/clacky/default_extensions/meeting/handler.rb +331 -0
  16. data/lib/clacky/default_extensions/meeting/meeting.js +790 -0
  17. data/lib/clacky/default_extensions/meeting/meta.yml +3 -0
  18. data/lib/clacky/default_extensions/meeting/skills/meeting-summarizer/SKILL.md +44 -0
  19. data/lib/clacky/default_skills/media-gen/SKILL.md +63 -0
  20. data/lib/clacky/default_skills/media-gen/scripts/video_seq.sh +114 -0
  21. data/lib/clacky/json_ui_controller.rb +1 -1
  22. data/lib/clacky/media/base.rb +60 -0
  23. data/lib/clacky/media/dashscope.rb +385 -21
  24. data/lib/clacky/media/gemini.rb +9 -0
  25. data/lib/clacky/media/generator.rb +52 -0
  26. data/lib/clacky/media/openai_compat.rb +166 -0
  27. data/lib/clacky/null_ui_controller.rb +13 -0
  28. data/lib/clacky/plain_ui_controller.rb +1 -1
  29. data/lib/clacky/providers.rb +50 -2
  30. data/lib/clacky/rich_ui/rich_ui_controller.rb +1 -1
  31. data/lib/clacky/server/channel/channel_ui_controller.rb +1 -1
  32. data/lib/clacky/server/http_server.rb +260 -122
  33. data/lib/clacky/server/session_registry.rb +54 -3
  34. data/lib/clacky/server/web_ui_controller.rb +3 -2
  35. data/lib/clacky/session_manager.rb +35 -4
  36. data/lib/clacky/skill_loader.rb +14 -2
  37. data/lib/clacky/tools/terminal/output_cleaner.rb +1 -3
  38. data/lib/clacky/tools/terminal.rb +0 -43
  39. data/lib/clacky/ui2/components/modal_component.rb +1 -1
  40. data/lib/clacky/ui2/layout_manager.rb +0 -5
  41. data/lib/clacky/ui2/progress_handle.rb +0 -3
  42. data/lib/clacky/ui2/ui_controller.rb +140 -40
  43. data/lib/clacky/ui_interface.rb +10 -1
  44. data/lib/clacky/utils/encoding.rb +25 -0
  45. data/lib/clacky/version.rb +1 -1
  46. data/lib/clacky/web/app.css +199 -23
  47. data/lib/clacky/web/components/onboard.js +1 -14
  48. data/lib/clacky/web/components/sidebar.js +1 -3
  49. data/lib/clacky/web/features/backup/store.js +23 -0
  50. data/lib/clacky/web/features/backup/view.js +49 -22
  51. data/lib/clacky/web/features/brand/view.js +8 -5
  52. data/lib/clacky/web/features/channels/store.js +1 -20
  53. data/lib/clacky/web/features/mcp/store.js +1 -20
  54. data/lib/clacky/web/features/profile/store.js +1 -13
  55. data/lib/clacky/web/features/profile/view.js +16 -4
  56. data/lib/clacky/web/features/skills/store.js +6 -21
  57. data/lib/clacky/web/features/tasks/view.js +77 -28
  58. data/lib/clacky/web/features/version/store.js +2 -0
  59. data/lib/clacky/web/i18n.js +46 -3
  60. data/lib/clacky/web/index.html +67 -26
  61. data/lib/clacky/web/sessions.js +177 -87
  62. data/lib/clacky/web/settings.js +34 -2
  63. data/lib/clacky/web/ws-dispatcher.js +12 -4
  64. data/lib/clacky.rb +12 -5
  65. metadata +8 -1
@@ -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;
@@ -2456,6 +2456,36 @@ body {
2456
2456
  .msg-copy-btn { opacity: 0.65; }
2457
2457
  }
2458
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
+
2459
2489
  /* ── Markdown rendering inside assistant messages ────────────────────────── */
2460
2490
  .msg-assistant p:last-child { margin-bottom: 0; }
2461
2491
  .msg-assistant h1, .msg-assistant h2, .msg-assistant h3,
@@ -2690,7 +2720,7 @@ body {
2690
2720
  align-self: flex-start;
2691
2721
  display: flex;
2692
2722
  flex-direction: column;
2693
- gap: 0.75rem;
2723
+ gap: 0.5rem;
2694
2724
  }
2695
2725
  .msg-error .retry-btn {
2696
2726
  align-self: flex-start;
@@ -2721,6 +2751,56 @@ body {
2721
2751
  .msg-error a:hover {
2722
2752
  opacity: 0.8;
2723
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
+ }
2724
2804
  .msg-success { color: var(--color-success); align-self: flex-start; font-size: 0.8125rem; }
2725
2805
  .tool-name { color: var(--color-warning); font-weight: 600; }
2726
2806
  .progress-msg { color: var(--color-accent-primary); font-size: 0.75rem; align-self: center; }
@@ -4406,6 +4486,7 @@ body {
4406
4486
  border-radius: 10px;
4407
4487
  padding: 0.625rem 0.875rem;
4408
4488
  background: var(--color-bg-secondary);
4489
+ margin-bottom: 0.625rem;
4409
4490
  }
4410
4491
  .backup-auto-card .backup-auto-row {
4411
4492
  margin: 0.25rem 0;
@@ -4437,6 +4518,43 @@ body {
4437
4518
  font-size: 0.75rem;
4438
4519
  color: var(--color-text-secondary);
4439
4520
  }
4521
+ .backup-dest-row {
4522
+ display: flex;
4523
+ align-items: center;
4524
+ gap: 0.5rem;
4525
+ margin: 0.625rem 0 0.25rem;
4526
+ padding-top: 0.625rem;
4527
+ border-top: 1px solid var(--color-border-primary);
4528
+ }
4529
+ .backup-dest-label {
4530
+ font-size: 0.78125rem;
4531
+ color: var(--color-text-secondary);
4532
+ white-space: nowrap;
4533
+ flex-shrink: 0;
4534
+ }
4535
+ .backup-dest-path {
4536
+ font-size: 0.78125rem;
4537
+ color: var(--color-text-primary);
4538
+ font-family: var(--font-mono, monospace);
4539
+ overflow: hidden;
4540
+ text-overflow: ellipsis;
4541
+ white-space: nowrap;
4542
+ }
4543
+ .btn-settings-link {
4544
+ flex-shrink: 0;
4545
+ background: transparent;
4546
+ border: 1px solid var(--color-border-primary);
4547
+ border-radius: 6px;
4548
+ color: var(--color-accent-primary);
4549
+ font-size: 0.75rem;
4550
+ padding: 0.25rem 0.625rem;
4551
+ cursor: pointer;
4552
+ white-space: nowrap;
4553
+ }
4554
+ .btn-settings-link:hover {
4555
+ background: var(--color-bg-hover);
4556
+ border-color: var(--color-accent-primary);
4557
+ }
4440
4558
  .backup-actions {
4441
4559
  display: flex;
4442
4560
  align-items: center;
@@ -4444,6 +4562,21 @@ body {
4444
4562
  flex-wrap: wrap;
4445
4563
  }
4446
4564
 
4565
+ .backup-config-row {
4566
+ margin-top: 0.5rem;
4567
+ padding-top: 0.75rem;
4568
+ border-top: 1px solid var(--color-border);
4569
+ }
4570
+ .btn-settings-action--file {
4571
+ cursor: pointer;
4572
+ }
4573
+ .backup-actions-sep {
4574
+ width: 1px;
4575
+ height: 1.25rem;
4576
+ background: var(--color-border);
4577
+ flex-shrink: 0;
4578
+ }
4579
+
4447
4580
  .settings-network-url {
4448
4581
  display: flex;
4449
4582
  flex-direction: column;
@@ -6929,10 +7062,10 @@ body {
6929
7062
  background: var(--color-bg-hover);
6930
7063
  }
6931
7064
  .code-editor-body .cm-activeLine {
6932
- background: var(--color-bg-hover);
7065
+ background: color-mix(in srgb, var(--color-bg-hover) 60%, transparent);
6933
7066
  }
6934
7067
  .code-editor-body .cm-selectionBackground {
6935
- background: color-mix(in srgb, var(--color-accent-primary) 20%, transparent) !important;
7068
+ background: color-mix(in srgb, var(--color-accent-primary) 40%, transparent) !important;
6936
7069
  }
6937
7070
  .code-editor-footer {
6938
7071
  display: flex;
@@ -7468,7 +7601,7 @@ body.setup-mode[data-theme="dark"] {
7468
7601
  cursor: pointer;
7469
7602
  transition: background .15s, opacity .15s;
7470
7603
  }
7471
- .setup-submit-btn:hover { background: #2563eb; }
7604
+ .setup-submit-btn:hover { background: var(--color-button-primary-hover); }
7472
7605
  .setup-submit-btn:disabled { opacity: .5; cursor: default; }
7473
7606
 
7474
7607
  /* ── Onboard panel ───────────────────────────────────────────────────────── */
@@ -10046,7 +10179,7 @@ body.setup-mode[data-theme="dark"] {
10046
10179
  border-right: 1px solid var(--color-border-primary);
10047
10180
  }
10048
10181
  .billing-period-btn:hover {
10049
- background: var(--color-bg-tertiary);
10182
+ background: var(--color-bg-hover);
10050
10183
  color: var(--color-text-primary);
10051
10184
  }
10052
10185
  .billing-period-btn.active {
@@ -10906,7 +11039,7 @@ body.setup-mode[data-theme="dark"] {
10906
11039
  white-space: nowrap;
10907
11040
  transition: opacity 0.15s ease, transform 0.05s ease;
10908
11041
  }
10909
- .btn-mcp-cta:hover { opacity: 0.85; }
11042
+ .btn-mcp-cta:hover { background: var(--color-button-primary-hover) }
10910
11043
  .btn-mcp-cta:active { transform: scale(0.98); }
10911
11044
  .btn-mcp-cta-large {
10912
11045
  padding: 0.625rem 1.125rem;
@@ -11019,6 +11152,49 @@ body.setup-mode[data-theme="dark"] {
11019
11152
  color: var(--color-error, #d33);
11020
11153
  }
11021
11154
 
11155
+ /* ── Accent Color Swatches ───────────────────────────────────────────── */
11156
+ .settings-accent-swatches {
11157
+ display: flex;
11158
+ flex-wrap: wrap;
11159
+ gap: 0.5rem;
11160
+ padding: 0.25rem 0;
11161
+ }
11162
+
11163
+ .settings-accent-swatch {
11164
+ width: 2.25rem;
11165
+ height: 2.25rem;
11166
+ border-radius: 50%;
11167
+ border: none;
11168
+ cursor: pointer;
11169
+ padding: 0;
11170
+ transition: transform 0.15s;
11171
+ outline: none;
11172
+ position: relative;
11173
+ }
11174
+
11175
+ .settings-accent-swatch:hover {
11176
+ transform: scale(1.15);
11177
+ }
11178
+
11179
+ .settings-accent-swatch.active::after {
11180
+ content: "";
11181
+ position: absolute;
11182
+ width: 0.375rem;
11183
+ height: 0.625rem;
11184
+ border-right: 0.125rem solid white;
11185
+ border-bottom: 0.125rem solid white;
11186
+ top: 46%;
11187
+ left: 50%;
11188
+ transform: translate(-50%, -50%) rotate(45deg);
11189
+ }
11190
+
11191
+ .swatch-indigo { background: #4f46e5; }
11192
+ .swatch-aurora-blue { background: #3B82F6; }
11193
+ .swatch-forest-green { background: #10B981; }
11194
+ .swatch-sunrise-orange { background: #F59E0B; }
11195
+ .swatch-rose-violet { background: #8B5CF6; }
11196
+ .swatch-coral-red { background: #EF4444; }
11197
+
11022
11198
  /* ── Sessions List ───────────────────────────────────────────────────── */
11023
11199
  .billing-sessions-row {
11024
11200
  margin-top: 1rem;
@@ -575,20 +575,7 @@ const Onboard = (() => {
575
575
  async function _launchOnboardSession() {
576
576
  try {
577
577
  await _complete();
578
- const res = await fetch("/api/sessions", {
579
- method: "POST",
580
- headers: { "Content-Type": "application/json" },
581
- body: JSON.stringify({ name: "Onboard", source: "setup" })
582
- });
583
- const data = await res.json();
584
- const session = data.session;
585
- if (!session) throw new Error("No session returned");
586
-
587
- Sessions.add(session);
588
- Sessions.renderList();
589
- Sessions.setPendingMessage(session.id, `/onboard lang:${_selectedLang}`);
590
- Sessions.select(session.id);
591
-
578
+ await Sessions.startWith(`/onboard lang:${_selectedLang}`, { name: "Onboard", source: "setup" });
592
579
  _bootUI();
593
580
  } catch (_) {
594
581
  // Fallback: just boot normally if session creation fails
@@ -13,9 +13,7 @@ const Sidebar = (() => {
13
13
  function init() {
14
14
  // Settings button toggles between "settings" and "welcome" view.
15
15
  document.getElementById("btn-settings").addEventListener("click", () => {
16
- if (Router.current === "settings") {
17
- Router.navigate("welcome");
18
- } else {
16
+ if (Router.current !== "settings") {
19
17
  Router.navigate("settings");
20
18
  }
21
19
  });
@@ -66,6 +66,29 @@ const BackupStore = (() => {
66
66
  }
67
67
  },
68
68
 
69
+ async openFolder() {
70
+ try {
71
+ await fetch("/api/backup/open-folder", { method: "POST" });
72
+ } catch (e) {
73
+ // Non-critical, fail quietly.
74
+ }
75
+ },
76
+
77
+ /** Upload a tar.gz archive to restore ~/.clacky. Returns { ok, error }. */
78
+ async restore(arrayBuffer) {
79
+ try {
80
+ const res = await fetch("/api/backup/restore", {
81
+ method: "POST",
82
+ headers: { "Content-Type": "application/octet-stream" },
83
+ body: arrayBuffer
84
+ });
85
+ const data = await res.json().catch(() => ({}));
86
+ return res.ok ? { ok: true } : { ok: false, error: data.error || "Restore failed" };
87
+ } catch (e) {
88
+ return { ok: false, error: e.message };
89
+ }
90
+ },
91
+
69
92
  /** Fetch a one-off archive. Returns { ok, blob, filename, error }. */
70
93
  async fetchArchive() {
71
94
  try {
@@ -1,17 +1,8 @@
1
- // ── Backup · view — settings rendering, toggles, download UI ──────────────
2
- //
3
- // Renders the backup settings panel, wires the toggles + download button, and
4
- // drives the file download from a blob. Reads through BackupStore.state; all
5
- // I/O goes through store actions. Re-renders on store change events.
6
- //
7
- // Augments the `Backup` facade with load (re-exposing the store action so the
8
- // existing Settings caller keeps working).
9
- //
10
- // Depends on: BackupStore, I18n.
11
- // ───────────────────────────────────────────────────────────────────────────
1
+ // ── Backup · view — settings rendering, toggles, download/restore UI ──────
12
2
 
13
3
  const BackupView = (() => {
14
4
  const $ = (id) => document.getElementById(id);
5
+ let _restoreStatusEl = null;
15
6
 
16
7
  function _fmtDate(iso) {
17
8
  if (!iso) return "";
@@ -19,8 +10,12 @@ const BackupView = (() => {
19
10
  }
20
11
 
21
12
  function _render() {
22
- const cfg = Backup.state.config;
23
- if (!Backup.state.status) return;
13
+ const cfg = Backup.state.config;
14
+ const status = Backup.state.status;
15
+ if (!status) return;
16
+
17
+ const autoToggle = $("backup-auto-toggle");
18
+ if (autoToggle) autoToggle.checked = !!cfg.enabled;
24
19
 
25
20
  const incl = $("backup-include-sessions");
26
21
  if (incl) {
@@ -28,8 +23,8 @@ const BackupView = (() => {
28
23
  incl.disabled = !cfg.enabled;
29
24
  }
30
25
 
31
- const autoToggle = $("backup-auto-toggle");
32
- if (autoToggle) autoToggle.checked = !!cfg.enabled;
26
+ const destPath = $("backup-dest-path");
27
+ if (destPath) destPath.textContent = status.dest_dir || "—";
33
28
 
34
29
  _renderLastRun(cfg);
35
30
  }
@@ -48,10 +43,10 @@ const BackupView = (() => {
48
43
  }
49
44
 
50
45
  async function _downloadNow() {
51
- const btn = $("btn-backup-now");
52
- const el = $("backup-status");
46
+ const btn = $("btn-backup-now");
47
+ const statusEl = $("backup-manual-status");
53
48
  if (btn) btn.disabled = true;
54
- if (el) { el.textContent = I18n.t("settings.backup.running"); el.className = "model-test-result"; }
49
+ if (statusEl) { statusEl.textContent = I18n.t("settings.backup.downloading"); statusEl.className = "model-test-result"; }
55
50
 
56
51
  const res = await Backup.fetchArchive();
57
52
  if (res.ok) {
@@ -63,14 +58,32 @@ const BackupView = (() => {
63
58
  a.click();
64
59
  a.remove();
65
60
  URL.revokeObjectURL(url);
66
- if (el) { el.textContent = I18n.t("settings.backup.downloaded"); el.className = "model-test-result success"; }
67
- } else if (el) {
68
- el.textContent = I18n.t("settings.backup.lastError", { msg: res.error });
69
- el.className = "model-test-result error";
61
+ if (statusEl) { statusEl.textContent = I18n.t("settings.backup.downloaded"); statusEl.className = "model-test-result success"; }
62
+ } else if (statusEl) {
63
+ statusEl.textContent = I18n.t("settings.backup.lastError", { msg: res.error });
64
+ statusEl.className = "model-test-result error";
70
65
  }
71
66
  if (btn) btn.disabled = false;
72
67
  }
73
68
 
69
+ async function _restoreBackup(e) {
70
+ const file = e.target.files[0];
71
+ if (!file) return;
72
+ e.target.value = "";
73
+
74
+ const statusEl = $("backup-manual-status");
75
+ if (statusEl) { statusEl.textContent = I18n.t("settings.backup.restoring"); statusEl.className = "model-test-result"; }
76
+
77
+ const buf = await file.arrayBuffer();
78
+ const res = await Backup.restore(buf);
79
+ if (res.ok) {
80
+ if (statusEl) { statusEl.textContent = I18n.t("settings.backup.restoreOk"); statusEl.className = "model-test-result success"; }
81
+ _restoreStatusEl = statusEl;
82
+ } else {
83
+ if (statusEl) { statusEl.textContent = res.error || "Restore failed"; statusEl.className = "model-test-result error"; }
84
+ }
85
+ }
86
+
74
87
  function _bind() {
75
88
  const btn = $("btn-backup-now");
76
89
  if (btn) btn.addEventListener("click", _downloadNow);
@@ -80,11 +93,25 @@ const BackupView = (() => {
80
93
 
81
94
  const incl = $("backup-include-sessions");
82
95
  if (incl) incl.addEventListener("change", () => Backup.saveConfig({ include_sessions: incl.checked }));
96
+
97
+ const openBtn = $("btn-backup-open-folder");
98
+ if (openBtn) openBtn.addEventListener("click", () => Backup.openFolder());
99
+
100
+ const restoreInput = $("input-backup-restore");
101
+ if (restoreInput) restoreInput.addEventListener("change", _restoreBackup);
83
102
  }
84
103
 
85
104
  function _subscribe() {
86
105
  Backup.on("backup:changed", _render);
87
106
  document.addEventListener("DOMContentLoaded", _bind);
107
+ WS.onEvent(ev => {
108
+ if (ev.type !== "_ws_connected" || !_restoreStatusEl) return;
109
+ const el = _restoreStatusEl;
110
+ _restoreStatusEl = null;
111
+ el.textContent = I18n.t("settings.backup.restartOk");
112
+ el.className = "model-test-result success";
113
+ if (typeof Settings !== "undefined") Settings.open();
114
+ });
88
115
  }
89
116
 
90
117
  return { init: _subscribe };
@@ -18,6 +18,7 @@ const BrandView = (() => {
18
18
  if (!data || !data.branded) return;
19
19
 
20
20
  if (data.needs_activation) {
21
+ if (data.theme_color) Settings.applyAccentColor(data.theme_color, { persist: true });
21
22
  _showActivationBanner(data.product_name);
22
23
  _applyHeaderLogo();
23
24
  if (data.distribution_refresh_pending) _scheduleDistributionRefreshPoll();
@@ -153,6 +154,7 @@ const BrandView = (() => {
153
154
  if (data.ok) {
154
155
  _setResult(true, I18n.t("brand.activate.success"));
155
156
  if (data.product_name) _applyBrandName(data.product_name);
157
+ if (data.theme_color) Settings.applyAccentColor(data.theme_color, { persist: true });
156
158
  Brand.clearBrandCache();
157
159
  _applyHeaderLogo();
158
160
  setTimeout(_bootUI, 800);
@@ -201,13 +203,14 @@ const BrandView = (() => {
201
203
  const brandWrap = document.getElementById("header-brand");
202
204
 
203
205
  if (info.theme_color) {
204
- const root = document.documentElement;
205
- root.style.setProperty("--color-accent-primary", info.theme_color);
206
- root.style.setProperty("--color-accent-hover", info.theme_color);
207
- root.style.setProperty("--color-button-primary", info.theme_color);
208
- root.style.setProperty("--color-button-primary-hover", info.theme_color);
209
206
  const metaTheme = document.querySelector("meta[name='theme-color']");
210
207
  if (metaTheme) metaTheme.setAttribute("content", info.theme_color);
208
+ }
209
+ const userAccent = (() => { try { return localStorage.getItem("clacky-accent-color"); } catch (_) { return null; } })();
210
+ if (userAccent) {
211
+ Settings.applyAccentColor(userAccent, { persist: false });
212
+ } else if (info.theme_color) {
213
+ Settings.applyAccentColor(info.theme_color, { persist: true });
211
214
  } else {
212
215
  const root = document.documentElement;
213
216
  root.style.removeProperty("--color-accent-primary");
@@ -37,26 +37,7 @@ const ChannelsStore = (() => {
37
37
  // Create a session, register it, queue a command, and navigate to it.
38
38
  async function _sendToAgent(command, sessionName) {
39
39
  try {
40
- const maxN = Sessions.all.reduce((max, s) => {
41
- const m = s.name.match(/^Session (\d+)$/);
42
- return m ? Math.max(max, parseInt(m[1], 10)) : max;
43
- }, 0);
44
- const name = sessionName || ("Session " + (maxN + 1));
45
-
46
- const res = await fetch("/api/sessions", {
47
- method: "POST",
48
- headers: { "Content-Type": "application/json" },
49
- body: JSON.stringify({ name, source: "setup" }),
50
- });
51
- const data = await res.json();
52
- if (!res.ok) throw new Error(data.error || I18n.t("channels.sessionError"));
53
- const session = data.session;
54
- if (!session) throw new Error(I18n.t("channels.noSession"));
55
-
56
- Sessions.add(session);
57
- Sessions.renderList();
58
- Sessions.setPendingMessage(session.id, command);
59
- Sessions.select(session.id);
40
+ await Sessions.startWith(command, { name: sessionName });
60
41
  } catch (e) {
61
42
  alert("Error: " + e.message);
62
43
  }
@@ -43,26 +43,7 @@ const McpStore = (() => {
43
43
 
44
44
  async function _sendToAgent(command, sessionName) {
45
45
  try {
46
- const maxN = Sessions.all.reduce((max, s) => {
47
- const m = s.name.match(/^Session (\d+)$/);
48
- return m ? Math.max(max, parseInt(m[1], 10)) : max;
49
- }, 0);
50
- const name = sessionName || ("Session " + (maxN + 1));
51
-
52
- const res = await fetch("/api/sessions", {
53
- method: "POST",
54
- headers: { "Content-Type": "application/json" },
55
- body: JSON.stringify({ name, source: "mcp" }),
56
- });
57
- const data = await res.json();
58
- if (!res.ok) throw new Error(data.error || "failed to create session");
59
- const session = data.session;
60
- if (!session) throw new Error("no session returned");
61
-
62
- Sessions.add(session);
63
- Sessions.renderList();
64
- Sessions.setPendingMessage(session.id, command);
65
- Sessions.select(session.id);
46
+ await Sessions.startWith(command, { name: sessionName });
66
47
  } catch (e) {
67
48
  alert("Error: " + e.message);
68
49
  }
@@ -64,19 +64,7 @@ const ProfileStore = (() => {
64
64
  }
65
65
 
66
66
  async function _openCurateSession(name, command) {
67
- const res = await fetch("/api/sessions", {
68
- method: "POST",
69
- headers: { "Content-Type": "application/json" },
70
- body: JSON.stringify({ name, source: "onboard" })
71
- });
72
- const data = await res.json();
73
- const session = data.session;
74
- if (!session) throw new Error("No session returned");
75
-
76
- Sessions.add(session);
77
- Sessions.renderList();
78
- Sessions.setPendingMessage(session.id, command);
79
- Sessions.select(session.id);
67
+ await Sessions.startWith(command, { name, source: "onboard" });
80
68
  }
81
69
 
82
70
  const Profile = {