openclacky 1.3.1 → 1.3.3
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 +4 -4
- data/CHANGELOG.md +44 -0
- data/Dockerfile +3 -0
- data/README.md +1 -1
- data/README_JA.md +237 -0
- data/lib/clacky/agent/session_serializer.rb +65 -11
- data/lib/clacky/agent/time_machine.rb +247 -26
- data/lib/clacky/agent.rb +12 -1
- data/lib/clacky/agent_config.rb +14 -2
- data/lib/clacky/brand_config.rb +1 -1
- data/lib/clacky/default_agents/_panels/git/panel.js +201 -0
- data/lib/clacky/default_agents/_panels/time_machine/panel.js +640 -0
- data/lib/clacky/default_agents/coding/profile.yml +3 -0
- data/lib/clacky/default_agents/coding/webui/.gitkeep +0 -0
- data/lib/clacky/default_skills/cron-task-creator/SKILL.md +1 -1
- data/lib/clacky/default_skills/extend-openclacky/SKILL.md +6 -4
- data/lib/clacky/default_skills/media-gen/SKILL.md +30 -6
- data/lib/clacky/media/openai_compat.rb +64 -1
- data/lib/clacky/media/output_dir.rb +43 -0
- data/lib/clacky/message_history.rb +9 -0
- data/lib/clacky/server/channel/channel_manager.rb +26 -0
- data/lib/clacky/server/git_panel.rb +115 -0
- data/lib/clacky/server/http_server.rb +521 -13
- data/lib/clacky/server/server_master.rb +6 -4
- data/lib/clacky/utils/environment_detector.rb +16 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +512 -60
- data/lib/clacky/web/app.js +30 -7
- data/lib/clacky/web/components/code-editor.js +197 -0
- data/lib/clacky/web/{notify.js → components/notify.js} +1 -1
- data/lib/clacky/web/core/aside.js +112 -0
- data/lib/clacky/web/core/ext.js +387 -0
- data/lib/clacky/web/features/backup/store.js +92 -0
- data/lib/clacky/web/features/backup/view.js +94 -0
- data/lib/clacky/web/features/billing/store.js +163 -0
- data/lib/clacky/web/{billing.js → features/billing/view.js} +134 -242
- data/lib/clacky/web/features/brand/store.js +110 -0
- data/lib/clacky/web/{brand.js → features/brand/view.js} +49 -199
- data/lib/clacky/web/features/channels/store.js +103 -0
- data/lib/clacky/web/{channels.js → features/channels/view.js} +50 -127
- data/lib/clacky/web/features/creator/store.js +81 -0
- data/lib/clacky/web/{creator.js → features/creator/view.js} +53 -102
- data/lib/clacky/web/features/mcp/store.js +158 -0
- data/lib/clacky/web/{mcp.js → features/mcp/view.js} +57 -134
- data/lib/clacky/web/features/model-tester/store.js +77 -0
- data/lib/clacky/web/features/model-tester/view.js +7 -0
- data/lib/clacky/web/features/profile/store.js +170 -0
- data/lib/clacky/web/{profile.js → features/profile/view.js} +94 -144
- data/lib/clacky/web/features/share/store.js +145 -0
- data/lib/clacky/web/{share.js → features/share/view.js} +66 -202
- data/lib/clacky/web/features/skills/store.js +303 -0
- data/lib/clacky/web/features/skills/view.js +550 -0
- data/lib/clacky/web/features/tasks/store.js +135 -0
- data/lib/clacky/web/features/tasks/view.js +241 -0
- data/lib/clacky/web/features/trash/store.js +242 -0
- data/lib/clacky/web/{trash.js → features/trash/view.js} +102 -293
- data/lib/clacky/web/features/version/store.js +165 -0
- data/lib/clacky/web/features/version/view.js +323 -0
- data/lib/clacky/web/features/workspace/store.js +99 -0
- data/lib/clacky/web/features/workspace/view.js +305 -0
- data/lib/clacky/web/i18n.js +60 -6
- data/lib/clacky/web/index.html +117 -57
- data/lib/clacky/web/sessions.js +221 -25
- data/lib/clacky/web/settings.js +121 -25
- data/lib/clacky/web/skills.js +3 -821
- data/lib/clacky/web/vendor/codemirror/codemirror.min.js +29 -0
- data/lib/clacky.rb +1 -0
- metadata +45 -20
- data/lib/clacky/web/backup.js +0 -119
- data/lib/clacky/web/model-tester.js +0 -66
- data/lib/clacky/web/tasks.js +0 -365
- data/lib/clacky/web/version.js +0 -449
- data/lib/clacky/web/workspace.js +0 -212
- /data/lib/clacky/web/{notify.mp3 → assets/notify.mp3} +0 -0
- /data/lib/clacky/web/{datepicker.js → components/datepicker.js} +0 -0
- /data/lib/clacky/web/{onboard.js → components/onboard.js} +0 -0
- /data/lib/clacky/web/{sidebar.js → components/sidebar.js} +0 -0
- /data/lib/clacky/web/{marked.min.js → vendor/marked/marked.min.js} +0 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// ── Brand · store — white-label status flags + brand info cache + network ──
|
|
2
|
+
//
|
|
3
|
+
// Owns the brand status flags (test mode / user-licensed / branded), the cached
|
|
4
|
+
// /api/brand response, and the brand network calls (status, info, activate).
|
|
5
|
+
// It never renders.
|
|
6
|
+
//
|
|
7
|
+
// check() / refresh() fetch status and emit events; the view reacts to drive
|
|
8
|
+
// banners, logo, badges. Emits mirror to the extension bus via Clacky.ext.emit.
|
|
9
|
+
//
|
|
10
|
+
// `Brand` stays the single public facade.
|
|
11
|
+
//
|
|
12
|
+
// Depends on: Clacky.ext.
|
|
13
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
const BrandStore = (() => {
|
|
16
|
+
let _testMode = false;
|
|
17
|
+
let _userLicensed = false;
|
|
18
|
+
let _branded = false;
|
|
19
|
+
|
|
20
|
+
let _brandInfoCache = null;
|
|
21
|
+
let _brandInfoFetching = null;
|
|
22
|
+
|
|
23
|
+
const _listeners = {};
|
|
24
|
+
|
|
25
|
+
function _on(event, handler) {
|
|
26
|
+
(_listeners[event] ||= []).push(handler);
|
|
27
|
+
return () => {
|
|
28
|
+
const list = _listeners[event];
|
|
29
|
+
const i = list ? list.indexOf(handler) : -1;
|
|
30
|
+
if (i >= 0) list.splice(i, 1);
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function _emit(event, payload) {
|
|
35
|
+
(_listeners[event] || []).forEach((h) => h(payload));
|
|
36
|
+
if (window.Clacky && Clacky.ext) Clacky.ext.emit(event, payload);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Fetch /api/brand once and cache. Returns a Promise<info>.
|
|
40
|
+
function _fetchBrandInfo() {
|
|
41
|
+
if (_brandInfoCache) return Promise.resolve(_brandInfoCache);
|
|
42
|
+
if (_brandInfoFetching) return _brandInfoFetching;
|
|
43
|
+
_brandInfoFetching = fetch("/api/brand")
|
|
44
|
+
.then(r => r.json())
|
|
45
|
+
.then(info => { _brandInfoCache = info; _brandInfoFetching = null; return info; })
|
|
46
|
+
.catch(err => { _brandInfoFetching = null; throw err; });
|
|
47
|
+
return _brandInfoFetching;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const Brand = {
|
|
51
|
+
on: _on,
|
|
52
|
+
get testMode() { return _testMode; },
|
|
53
|
+
get userLicensed() { return _userLicensed; },
|
|
54
|
+
get branded() { return _branded; },
|
|
55
|
+
|
|
56
|
+
fetchInfo: _fetchBrandInfo,
|
|
57
|
+
|
|
58
|
+
clearBrandCache() {
|
|
59
|
+
_brandInfoCache = null;
|
|
60
|
+
_brandInfoFetching = null;
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
// Check brand status and emit an event for the view to act on.
|
|
64
|
+
// Always resolves false (boot is no longer deferred on activation).
|
|
65
|
+
async check() {
|
|
66
|
+
try {
|
|
67
|
+
const res = await fetch("/api/brand/status");
|
|
68
|
+
const data = await res.json();
|
|
69
|
+
_testMode = !!data.test_mode;
|
|
70
|
+
_userLicensed = !!data.user_licensed;
|
|
71
|
+
_branded = !!data.branded;
|
|
72
|
+
_emit("brand:status", data);
|
|
73
|
+
} catch (_) {
|
|
74
|
+
_emit("brand:status", null);
|
|
75
|
+
}
|
|
76
|
+
return false;
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
// Re-fetch status to refresh flags only (no UI boot driving).
|
|
80
|
+
async refresh() {
|
|
81
|
+
try {
|
|
82
|
+
const res = await fetch("/api/brand/status");
|
|
83
|
+
const data = await res.json();
|
|
84
|
+
_testMode = !!data.test_mode;
|
|
85
|
+
_userLicensed = !!data.user_licensed;
|
|
86
|
+
_branded = !!data.branded;
|
|
87
|
+
return data;
|
|
88
|
+
} catch (_) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
fetchSkillsBanner() {
|
|
94
|
+
return fetch("/api/brand/skills").then(r => r.json());
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
async activate(key) {
|
|
98
|
+
const res = await fetch("/api/brand/activate", {
|
|
99
|
+
method: "POST",
|
|
100
|
+
headers: { "Content-Type": "application/json" },
|
|
101
|
+
body: JSON.stringify({ license_key: key })
|
|
102
|
+
});
|
|
103
|
+
return res.json();
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
return Brand;
|
|
108
|
+
})();
|
|
109
|
+
|
|
110
|
+
const Brand = BrandStore;
|
|
@@ -1,76 +1,34 @@
|
|
|
1
|
-
//
|
|
1
|
+
// ── Brand · view — banners, logo/favicon, owner badge, activation panel ───
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// - If not branded → no-op (standard OpenClacky experience)
|
|
8
|
-
// 2. Fetch GET /api/brand and apply product_name to all branded DOM elements
|
|
3
|
+
// Owns all white-label DOM: activation banner, warning bar, header logo +
|
|
4
|
+
// favicon + theme color, OWNER badge, the "get serial" link, and the activation
|
|
5
|
+
// panel flow. Reads brand info through BrandStore; status/activation network
|
|
6
|
+
// calls go through store actions.
|
|
9
7
|
//
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
// Whether the server was started with --brand-test (set during check()).
|
|
17
|
-
let _testMode = false;
|
|
18
|
-
// Whether the current license is bound to a user (creator mode).
|
|
19
|
-
let _userLicensed = false;
|
|
20
|
-
// Whether this installation has an activated brand license (any kind).
|
|
21
|
-
let _branded = false;
|
|
22
|
-
|
|
23
|
-
// Check brand status. Returns true if activation is needed
|
|
24
|
-
// (caller should defer normal UI boot until activation is done or skipped).
|
|
25
|
-
async function check() {
|
|
26
|
-
try {
|
|
27
|
-
const res = await fetch("/api/brand/status");
|
|
28
|
-
const data = await res.json();
|
|
29
|
-
|
|
30
|
-
_testMode = !!data.test_mode;
|
|
31
|
-
_userLicensed = !!data.user_licensed;
|
|
32
|
-
_branded = !!data.branded;
|
|
33
|
-
|
|
34
|
-
if (!data.branded) return false;
|
|
35
|
-
|
|
36
|
-
// Brand name is already baked into the HTML by the server at request time,
|
|
37
|
-
// so no DOM update is needed here on boot.
|
|
38
|
-
|
|
39
|
-
if (data.needs_activation) {
|
|
40
|
-
_showActivationBanner(data.product_name);
|
|
41
|
-
_applyHeaderLogo();
|
|
42
|
-
|
|
43
|
-
// Backend just kicked off an async refresh of the public distribution
|
|
44
|
-
// (logo, theme_color, homepage_url, support_*). brand.yml will be
|
|
45
|
-
// written once the network round-trip completes — re-poll a few
|
|
46
|
-
// seconds later so the user sees the full brand visuals in *this*
|
|
47
|
-
// session instead of having to activate or reload the page first.
|
|
48
|
-
if (data.distribution_refresh_pending) {
|
|
49
|
-
_scheduleDistributionRefreshPoll();
|
|
50
|
-
}
|
|
8
|
+
// Augments the `Brand` facade with the apply* / goToLicenseInput methods that
|
|
9
|
+
// other modules call.
|
|
10
|
+
//
|
|
11
|
+
// Depends on: BrandStore, I18n, Router, Settings (optional), WS/Tasks/Skills
|
|
12
|
+
// (boot fallback), global $ helper.
|
|
13
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
51
14
|
|
|
52
|
-
|
|
53
|
-
}
|
|
15
|
+
const BrandView = (() => {
|
|
54
16
|
|
|
55
|
-
|
|
17
|
+
function _onStatus(data) {
|
|
18
|
+
if (!data || !data.branded) return;
|
|
56
19
|
|
|
57
|
-
|
|
20
|
+
if (data.needs_activation) {
|
|
21
|
+
_showActivationBanner(data.product_name);
|
|
58
22
|
_applyHeaderLogo();
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
return false;
|
|
63
|
-
} catch (_) {
|
|
64
|
-
return false;
|
|
23
|
+
if (data.distribution_refresh_pending) _scheduleDistributionRefreshPoll();
|
|
24
|
+
return;
|
|
65
25
|
}
|
|
66
|
-
}
|
|
67
26
|
|
|
68
|
-
|
|
27
|
+
if (data.warning) _showWarning(data.warning);
|
|
28
|
+
_applyHeaderLogo();
|
|
29
|
+
_applyOwnerBadge();
|
|
30
|
+
}
|
|
69
31
|
|
|
70
|
-
// Show a dismissible activation banner at the top of the page.
|
|
71
|
-
// Defers rendering until /api/brand/skills resolves so the banner shows its
|
|
72
|
-
// final copy in one shot. Falls back to a generic prompt if the API fails
|
|
73
|
-
// or stays silent for 5s.
|
|
74
32
|
function _showActivationBanner(brandName) {
|
|
75
33
|
if (document.getElementById("brand-activation-banner")) return;
|
|
76
34
|
|
|
@@ -84,11 +42,7 @@ const Brand = (() => {
|
|
|
84
42
|
_renderActivationBanner(name, data);
|
|
85
43
|
};
|
|
86
44
|
|
|
87
|
-
|
|
88
|
-
.then(r => r.json())
|
|
89
|
-
.then(settle)
|
|
90
|
-
.catch(() => settle(null));
|
|
91
|
-
|
|
45
|
+
Brand.fetchSkillsBanner().then(settle).catch(() => settle(null));
|
|
92
46
|
setTimeout(() => settle(null), 5000);
|
|
93
47
|
}
|
|
94
48
|
|
|
@@ -138,14 +92,11 @@ const Brand = (() => {
|
|
|
138
92
|
document.getElementById("main").prepend(bar);
|
|
139
93
|
}
|
|
140
94
|
|
|
141
|
-
// Navigate to Settings, scroll to Brand & License section, flash it, then focus the input.
|
|
142
95
|
function _goToLicenseInput() {
|
|
143
96
|
Router.navigate("settings");
|
|
144
|
-
// Settings.open() loads brand status; wait a tick for the panel to render.
|
|
145
97
|
if (typeof Settings !== "undefined") Settings.open();
|
|
146
|
-
// Settings.open() triggers an async fetch; wait for layout to stabilise before scrolling.
|
|
147
98
|
setTimeout(() => {
|
|
148
|
-
const generalTabBtn
|
|
99
|
+
const generalTabBtn = document.querySelector('#settings-tabs .settings-tab[data-tab="general"]');
|
|
149
100
|
if (generalTabBtn && !generalTabBtn.classList.contains("active")) generalTabBtn.click();
|
|
150
101
|
|
|
151
102
|
const section = document.getElementById("brand-license-section");
|
|
@@ -160,7 +111,6 @@ const Brand = (() => {
|
|
|
160
111
|
}
|
|
161
112
|
|
|
162
113
|
if (section) {
|
|
163
|
-
// Flash the section to draw the user's eye (re-trigger if clicked again).
|
|
164
114
|
section.classList.remove("section-highlight");
|
|
165
115
|
void section.offsetWidth; // force reflow to restart animation
|
|
166
116
|
section.classList.add("section-highlight");
|
|
@@ -171,17 +121,6 @@ const Brand = (() => {
|
|
|
171
121
|
}, 300);
|
|
172
122
|
}
|
|
173
123
|
|
|
174
|
-
function _showActivationPanel(brandName) {
|
|
175
|
-
if (brandName) {
|
|
176
|
-
const title = $("brand-title");
|
|
177
|
-
const sub = $("brand-subtitle");
|
|
178
|
-
if (title) title.textContent = I18n.t("brand.activate.title", { name: brandName });
|
|
179
|
-
if (sub) sub.textContent = I18n.t("brand.activate.subtitle");
|
|
180
|
-
}
|
|
181
|
-
Router.navigate("brand");
|
|
182
|
-
_bindActivationPanel();
|
|
183
|
-
}
|
|
184
|
-
|
|
185
124
|
function _bindActivationPanel() {
|
|
186
125
|
$("brand-btn-activate").addEventListener("click", _doActivate);
|
|
187
126
|
$("brand-license-key").addEventListener("keydown", e => {
|
|
@@ -199,8 +138,7 @@ const Brand = (() => {
|
|
|
199
138
|
return;
|
|
200
139
|
}
|
|
201
140
|
|
|
202
|
-
|
|
203
|
-
if (!_testMode && !/^[0-9A-Fa-f]{8}(-[0-9A-Fa-f]{8}){4}$/.test(key)) {
|
|
141
|
+
if (!Brand.testMode && !/^[0-9A-Fa-f]{8}(-[0-9A-Fa-f]{8}){4}$/.test(key)) {
|
|
204
142
|
_setResult(false, I18n.t("settings.brand.invalidFormat"));
|
|
205
143
|
return;
|
|
206
144
|
}
|
|
@@ -210,17 +148,12 @@ const Brand = (() => {
|
|
|
210
148
|
_setResult(null, "");
|
|
211
149
|
|
|
212
150
|
try {
|
|
213
|
-
const
|
|
214
|
-
method: "POST",
|
|
215
|
-
headers: { "Content-Type": "application/json" },
|
|
216
|
-
body: JSON.stringify({ license_key: key })
|
|
217
|
-
});
|
|
218
|
-
const data = await res.json();
|
|
151
|
+
const data = await Brand.activate(key);
|
|
219
152
|
|
|
220
153
|
if (data.ok) {
|
|
221
154
|
_setResult(true, I18n.t("brand.activate.success"));
|
|
222
155
|
if (data.product_name) _applyBrandName(data.product_name);
|
|
223
|
-
|
|
156
|
+
Brand.clearBrandCache();
|
|
224
157
|
_applyHeaderLogo();
|
|
225
158
|
setTimeout(_bootUI, 800);
|
|
226
159
|
} else {
|
|
@@ -236,8 +169,6 @@ const Brand = (() => {
|
|
|
236
169
|
}
|
|
237
170
|
|
|
238
171
|
function _skipActivation() {
|
|
239
|
-
// Show a dismissible warning so the user knows brand features are unavailable.
|
|
240
|
-
// Pass the i18n key so the bar text updates when the user switches language.
|
|
241
172
|
_showWarning(I18n.t("brand.skip.warning"), "brand.skip.warning");
|
|
242
173
|
_bootUI();
|
|
243
174
|
}
|
|
@@ -250,7 +181,6 @@ const Brand = (() => {
|
|
|
250
181
|
el.className = "onboard-test-result " + (ok ? "result-ok" : "result-fail");
|
|
251
182
|
}
|
|
252
183
|
|
|
253
|
-
// Replace all branded text nodes in the DOM.
|
|
254
184
|
function _applyBrandName(name) {
|
|
255
185
|
const nodes = {
|
|
256
186
|
"page-title": name,
|
|
@@ -264,40 +194,21 @@ const Brand = (() => {
|
|
|
264
194
|
});
|
|
265
195
|
}
|
|
266
196
|
|
|
267
|
-
// Cache for brand info to avoid redundant fetches and duplicate image loads.
|
|
268
|
-
let _brandInfoCache = null;
|
|
269
|
-
let _brandInfoFetching = null;
|
|
270
|
-
|
|
271
|
-
// Fetch /api/brand once and cache the result. Returns a Promise<info>.
|
|
272
|
-
function _fetchBrandInfo() {
|
|
273
|
-
if (_brandInfoCache) return Promise.resolve(_brandInfoCache);
|
|
274
|
-
if (_brandInfoFetching) return _brandInfoFetching;
|
|
275
|
-
_brandInfoFetching = fetch("/api/brand")
|
|
276
|
-
.then(r => r.json())
|
|
277
|
-
.then(info => { _brandInfoCache = info; _brandInfoFetching = null; return info; })
|
|
278
|
-
.catch(err => { _brandInfoFetching = null; throw err; });
|
|
279
|
-
return _brandInfoFetching;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
// Fetch /api/brand and apply logo_url + product_name to the header if available.
|
|
283
197
|
function _applyHeaderLogo() {
|
|
284
|
-
|
|
198
|
+
Brand.fetchInfo().then(info => {
|
|
285
199
|
const logoImg = document.getElementById("header-logo-img");
|
|
286
200
|
const logoText = document.getElementById("header-logo");
|
|
287
201
|
const brandWrap = document.getElementById("header-brand");
|
|
288
202
|
|
|
289
|
-
// Apply theme color — overrides --color-accent-primary and --color-button-primary
|
|
290
203
|
if (info.theme_color) {
|
|
291
204
|
const root = document.documentElement;
|
|
292
205
|
root.style.setProperty("--color-accent-primary", info.theme_color);
|
|
293
206
|
root.style.setProperty("--color-accent-hover", info.theme_color);
|
|
294
207
|
root.style.setProperty("--color-button-primary", info.theme_color);
|
|
295
208
|
root.style.setProperty("--color-button-primary-hover", info.theme_color);
|
|
296
|
-
// Also update browser tab color on mobile
|
|
297
209
|
const metaTheme = document.querySelector("meta[name='theme-color']");
|
|
298
210
|
if (metaTheme) metaTheme.setAttribute("content", info.theme_color);
|
|
299
211
|
} else {
|
|
300
|
-
// No brand theme — remove any previously applied overrides to restore defaults
|
|
301
212
|
const root = document.documentElement;
|
|
302
213
|
root.style.removeProperty("--color-accent-primary");
|
|
303
214
|
root.style.removeProperty("--color-accent-hover");
|
|
@@ -305,60 +216,46 @@ const Brand = (() => {
|
|
|
305
216
|
root.style.removeProperty("--color-button-primary-hover");
|
|
306
217
|
}
|
|
307
218
|
|
|
308
|
-
// header-brand already has onclick="Router.navigate('chat')" in HTML, no extra link needed
|
|
309
|
-
|
|
310
219
|
const hasLogo = !!(info.logo_url && logoImg);
|
|
311
220
|
|
|
312
221
|
if (hasLogo) {
|
|
313
222
|
if (logoImg.src && logoImg.src === info.logo_url) {
|
|
314
|
-
// Already showing the correct logo — only ensure favicon is in sync
|
|
315
223
|
_applyFavicon(info.logo_url);
|
|
316
224
|
} else {
|
|
317
|
-
// Pre-load the image; only show it once loaded to avoid layout flicker
|
|
318
225
|
const img = new Image();
|
|
319
226
|
img.onload = () => {
|
|
320
227
|
logoImg.src = info.logo_url;
|
|
321
228
|
logoImg.alt = info.product_name || "";
|
|
322
229
|
logoImg.style.display = "";
|
|
323
230
|
if (brandWrap) brandWrap.classList.add("has-logo");
|
|
324
|
-
// Update browser tab favicon to match the brand logo
|
|
325
231
|
_applyFavicon(info.logo_url);
|
|
326
232
|
};
|
|
327
|
-
img.onerror = () => {
|
|
328
|
-
// Logo failed to load — keep text-only mode
|
|
329
|
-
};
|
|
233
|
+
img.onerror = () => {};
|
|
330
234
|
img.src = info.logo_url;
|
|
331
235
|
}
|
|
332
236
|
} else if (info.product_name) {
|
|
333
|
-
// Brand configured but no logo — hide the image, brand name text is enough
|
|
334
237
|
if (logoImg) {
|
|
335
238
|
logoImg.style.display = "none";
|
|
336
239
|
logoImg.src = "";
|
|
337
240
|
}
|
|
338
241
|
if (brandWrap) brandWrap.classList.remove("has-logo");
|
|
339
242
|
} else {
|
|
340
|
-
// No brand at all — show default OpenClacky logo
|
|
341
243
|
_applyDefaultLogo();
|
|
342
244
|
}
|
|
343
245
|
|
|
344
|
-
// Always show brand name text; hide it only when no brand name is set
|
|
345
246
|
if (logoText) {
|
|
346
247
|
const name = info.product_name || "";
|
|
347
248
|
if (name) {
|
|
348
249
|
logoText.textContent = name;
|
|
349
250
|
logoText.style.display = "";
|
|
350
251
|
} else {
|
|
351
|
-
// No brand configured — show the default "OpenClacky" name
|
|
352
252
|
logoText.textContent = "OpenClacky";
|
|
353
253
|
logoText.style.display = "";
|
|
354
254
|
}
|
|
355
255
|
}
|
|
356
|
-
}).catch(() => {
|
|
357
|
-
// Silently ignore — logo is non-critical
|
|
358
|
-
});
|
|
256
|
+
}).catch(() => {});
|
|
359
257
|
}
|
|
360
258
|
|
|
361
|
-
// Apply the default OpenClacky logo based on current theme.
|
|
362
259
|
function _applyDefaultLogo() {
|
|
363
260
|
const logoImg = document.getElementById("header-logo-img");
|
|
364
261
|
const brandWrap = document.getElementById("header-brand");
|
|
@@ -370,8 +267,6 @@ const Brand = (() => {
|
|
|
370
267
|
if (brandWrap) brandWrap.classList.add("has-logo");
|
|
371
268
|
}
|
|
372
269
|
|
|
373
|
-
// Replace the browser tab favicon with the given URL.
|
|
374
|
-
// Works for both image URLs and SVG data URIs.
|
|
375
270
|
function _applyFavicon(url) {
|
|
376
271
|
let link = document.querySelector("link[rel='icon']");
|
|
377
272
|
if (!link) {
|
|
@@ -387,9 +282,6 @@ const Brand = (() => {
|
|
|
387
282
|
link.href = url;
|
|
388
283
|
}
|
|
389
284
|
|
|
390
|
-
// Show a dismissible warning bar above the main content.
|
|
391
|
-
// The i18n key is stored on the span so I18n.applyAll() can re-translate
|
|
392
|
-
// it when the user switches language without dismissing the bar.
|
|
393
285
|
function _showWarning(message, i18nKey) {
|
|
394
286
|
const existing = document.getElementById("brand-warning-bar");
|
|
395
287
|
if (existing) return;
|
|
@@ -411,51 +303,29 @@ const Brand = (() => {
|
|
|
411
303
|
document.getElementById("main").prepend(bar);
|
|
412
304
|
}
|
|
413
305
|
|
|
414
|
-
// Continue the boot sequence after brand check is resolved (activated or skipped).
|
|
415
|
-
// Delegates to window.bootAfterBrand() defined in app.js so the onboard check
|
|
416
|
-
// runs before WS.connect() — ensures key_setup is shown when no API key exists.
|
|
417
306
|
function _bootUI() {
|
|
418
307
|
if (typeof window.bootAfterBrand === "function") {
|
|
419
308
|
window.bootAfterBrand();
|
|
420
309
|
} else {
|
|
421
|
-
// Fallback: app.js not yet loaded, boot directly
|
|
422
310
|
WS.connect();
|
|
423
311
|
Tasks.load();
|
|
424
312
|
Skills.load();
|
|
425
313
|
}
|
|
426
314
|
}
|
|
427
315
|
|
|
428
|
-
// Bust the cached /api/brand response so the next applyHeaderLogo() call
|
|
429
|
-
// fetches fresh data from the server (needed after license key switch).
|
|
430
|
-
function _clearBrandCache() {
|
|
431
|
-
_brandInfoCache = null;
|
|
432
|
-
_brandInfoFetching = null;
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
// Poll /api/brand a few times to pick up freshly-refreshed distribution
|
|
436
|
-
// assets (logo_url / theme_color / homepage_url). Used on first boot of a
|
|
437
|
-
// branded-but-unactivated install, where /api/brand/status just kicked off
|
|
438
|
-
// an async distribution fetch — brand.yml will be written once the round-
|
|
439
|
-
// trip completes, and we want to apply the result in the current session.
|
|
440
|
-
//
|
|
441
|
-
// Delay schedule: 3s, 8s, 15s from now. Stops early once logo_url and
|
|
442
|
-
// theme_color are both present (assumed "fully refreshed").
|
|
443
|
-
// Safe to call multiple times: a second call is ignored while a poll chain
|
|
444
|
-
// is already in flight.
|
|
445
316
|
let _distRefreshPolling = false;
|
|
446
317
|
function _scheduleDistributionRefreshPoll() {
|
|
447
318
|
if (_distRefreshPolling) return;
|
|
448
319
|
_distRefreshPolling = true;
|
|
449
320
|
|
|
450
|
-
const delays = [3000, 5000, 7000];
|
|
321
|
+
const delays = [3000, 5000, 7000];
|
|
451
322
|
let attempt = 0;
|
|
452
323
|
|
|
453
324
|
const poll = () => {
|
|
454
|
-
|
|
455
|
-
|
|
325
|
+
Brand.clearBrandCache();
|
|
326
|
+
Brand.fetchInfo().then(info => {
|
|
456
327
|
const hasFullBrand = !!(info && info.logo_url && info.theme_color);
|
|
457
328
|
_applyHeaderLogo();
|
|
458
|
-
// Stop when we've got the full brand visuals, or we've exhausted retries.
|
|
459
329
|
if (hasFullBrand || attempt >= delays.length) {
|
|
460
330
|
_distRefreshPolling = false;
|
|
461
331
|
return;
|
|
@@ -470,30 +340,17 @@ const Brand = (() => {
|
|
|
470
340
|
setTimeout(poll, delays[attempt++]);
|
|
471
341
|
}
|
|
472
342
|
|
|
473
|
-
// Show or hide the OWNER badge next to the header logo, based on whether
|
|
474
|
-
// the current license is bound to a user (creator tier). Idempotent — safe
|
|
475
|
-
// to call multiple times. Should be invoked after any state change that
|
|
476
|
-
// affects userLicensed: initial check(), post-activation refresh(),
|
|
477
|
-
// post-unbind, etc.
|
|
478
343
|
function _applyOwnerBadge() {
|
|
479
344
|
const badge = document.getElementById("header-owner-badge");
|
|
480
345
|
if (!badge) return;
|
|
481
|
-
badge.style.display =
|
|
346
|
+
badge.style.display = Brand.userLicensed ? "" : "none";
|
|
482
347
|
}
|
|
483
348
|
|
|
484
|
-
// Show or hide the "Get a Serial Number" helper row in the activation form,
|
|
485
|
-
// driven by whether the brand vendor has published a homepage_url.
|
|
486
|
-
// URL source: /api/brand → homepage_url field (set by the brand partner
|
|
487
|
-
// via BrandConfig/distribution, never hardcoded in the client).
|
|
488
|
-
// Rules:
|
|
489
|
-
// - homepage_url present → row visible, button opens the URL
|
|
490
|
-
// - homepage_url missing/empty → row hidden (no dead link for
|
|
491
|
-
// unbranded or brand-without-homepage setups)
|
|
492
349
|
function _applyGetSerialLink() {
|
|
493
350
|
const row = document.getElementById("brand-get-serial");
|
|
494
351
|
const btn = document.getElementById("btn-get-serial");
|
|
495
352
|
if (!row || !btn) return;
|
|
496
|
-
|
|
353
|
+
Brand.fetchInfo().then(info => {
|
|
497
354
|
const url = info && typeof info.homepage_url === "string" ? info.homepage_url.trim() : "";
|
|
498
355
|
if (url) {
|
|
499
356
|
row.style.display = "";
|
|
@@ -507,28 +364,21 @@ const Brand = (() => {
|
|
|
507
364
|
});
|
|
508
365
|
}
|
|
509
366
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
// reads them (e.g. Creator.updateSidebarVisibility) sees fresh values
|
|
514
|
-
// after a license activation without a full page reload.
|
|
515
|
-
async function refresh() {
|
|
516
|
-
try {
|
|
517
|
-
const res = await fetch("/api/brand/status");
|
|
518
|
-
const data = await res.json();
|
|
519
|
-
_testMode = !!data.test_mode;
|
|
520
|
-
_userLicensed = !!data.user_licensed;
|
|
521
|
-
_branded = !!data.branded;
|
|
522
|
-
return data;
|
|
523
|
-
} catch (_) {
|
|
524
|
-
return null;
|
|
525
|
-
}
|
|
367
|
+
function _subscribe() {
|
|
368
|
+
Brand.on("brand:status", _onStatus);
|
|
369
|
+
window.addEventListener("clacky-theme-change", () => {});
|
|
526
370
|
}
|
|
527
371
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
372
|
+
const viewApi = {
|
|
373
|
+
applyBrandName: _applyBrandName,
|
|
374
|
+
applyHeaderLogo: _applyHeaderLogo,
|
|
375
|
+
applyOwnerBadge: _applyOwnerBadge,
|
|
376
|
+
applyGetSerialLink: _applyGetSerialLink,
|
|
377
|
+
goToLicenseInput: _goToLicenseInput,
|
|
378
|
+
};
|
|
532
379
|
|
|
533
|
-
return {
|
|
380
|
+
return { init: _subscribe, api: viewApi };
|
|
534
381
|
})();
|
|
382
|
+
|
|
383
|
+
Object.assign(Brand, BrandView.api);
|
|
384
|
+
BrandView.init();
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// ── Channels · store — channel status data + Agent-driven actions ──────────
|
|
2
|
+
//
|
|
3
|
+
// Channels is an Agent-First panel: no config forms. The store fetches platform
|
|
4
|
+
// status and runs "open a session and send a /channel-manager command" actions.
|
|
5
|
+
// It never renders — it emits events the view reacts to.
|
|
6
|
+
//
|
|
7
|
+
// Internal bus always live; Clacky.ext.emit mirrors to the extension bus.
|
|
8
|
+
//
|
|
9
|
+
// `Channels` stays the single public facade.
|
|
10
|
+
//
|
|
11
|
+
// Depends on: Sessions, I18n, Clacky.ext.
|
|
12
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
const ChannelsStore = (() => {
|
|
15
|
+
let _channels = [];
|
|
16
|
+
|
|
17
|
+
const _listeners = {};
|
|
18
|
+
|
|
19
|
+
function _on(event, handler) {
|
|
20
|
+
(_listeners[event] ||= []).push(handler);
|
|
21
|
+
return () => {
|
|
22
|
+
const list = _listeners[event];
|
|
23
|
+
const i = list ? list.indexOf(handler) : -1;
|
|
24
|
+
if (i >= 0) list.splice(i, 1);
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function _emit(event, payload) {
|
|
29
|
+
(_listeners[event] || []).forEach((h) => h(payload));
|
|
30
|
+
if (window.Clacky && Clacky.ext) Clacky.ext.emit(event, payload);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const state = {
|
|
34
|
+
get channels() { return _channels; },
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Create a session, register it, queue a command, and navigate to it.
|
|
38
|
+
async function _sendToAgent(command, sessionName) {
|
|
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);
|
|
60
|
+
} catch (e) {
|
|
61
|
+
alert("Error: " + e.message);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const Channels = {
|
|
66
|
+
on: _on,
|
|
67
|
+
state,
|
|
68
|
+
|
|
69
|
+
/** Fetch channel status; emit so the view re-renders. */
|
|
70
|
+
async load({ silent = false } = {}) {
|
|
71
|
+
if (!silent) _emit("channels:loading");
|
|
72
|
+
try {
|
|
73
|
+
const res = await fetch("/api/channels");
|
|
74
|
+
const data = await res.json();
|
|
75
|
+
_channels = data.channels || [];
|
|
76
|
+
_emit("channels:changed", { channels: _channels });
|
|
77
|
+
} catch (e) {
|
|
78
|
+
_emit("channels:error", { message: e.message });
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
/** Toggle a channel's enabled flag; reload silently on success. */
|
|
83
|
+
async toggle(platform, desired) {
|
|
84
|
+
const res = await fetch(`/api/channels/${encodeURIComponent(platform)}/enabled`, {
|
|
85
|
+
method: "PATCH",
|
|
86
|
+
headers: { "Content-Type": "application/json" },
|
|
87
|
+
body: JSON.stringify({ enabled: desired }),
|
|
88
|
+
});
|
|
89
|
+
const data = await res.json();
|
|
90
|
+
if (!res.ok || !data.ok) throw new Error(data.error || "toggle failed");
|
|
91
|
+
await Channels.load({ silent: true });
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
/** Open a session and run the channel doctor / setup commands. */
|
|
95
|
+
runTest(command, name) { return _sendToAgent(command, name); },
|
|
96
|
+
openSetup(command, name) { return _sendToAgent(command, name); },
|
|
97
|
+
sendToAgent: _sendToAgent,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
return Channels;
|
|
101
|
+
})();
|
|
102
|
+
|
|
103
|
+
const Channels = ChannelsStore;
|