openclacky 1.3.2 → 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.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +28 -0
  3. data/Dockerfile +3 -0
  4. data/README.md +1 -1
  5. data/README_JA.md +237 -0
  6. data/lib/clacky/agent/session_serializer.rb +49 -5
  7. data/lib/clacky/agent/time_machine.rb +247 -26
  8. data/lib/clacky/agent.rb +12 -1
  9. data/lib/clacky/agent_config.rb +14 -2
  10. data/lib/clacky/default_agents/_panels/git/panel.js +201 -0
  11. data/lib/clacky/default_agents/_panels/time_machine/panel.js +640 -0
  12. data/lib/clacky/default_agents/coding/profile.yml +3 -0
  13. data/lib/clacky/default_agents/coding/webui/.gitkeep +0 -0
  14. data/lib/clacky/default_skills/cron-task-creator/SKILL.md +1 -1
  15. data/lib/clacky/default_skills/extend-openclacky/SKILL.md +6 -4
  16. data/lib/clacky/default_skills/media-gen/SKILL.md +30 -6
  17. data/lib/clacky/media/openai_compat.rb +64 -1
  18. data/lib/clacky/media/output_dir.rb +43 -0
  19. data/lib/clacky/message_history.rb +9 -0
  20. data/lib/clacky/server/channel/channel_manager.rb +26 -0
  21. data/lib/clacky/server/git_panel.rb +115 -0
  22. data/lib/clacky/server/http_server.rb +497 -12
  23. data/lib/clacky/server/server_master.rb +6 -4
  24. data/lib/clacky/version.rb +1 -1
  25. data/lib/clacky/web/app.css +473 -60
  26. data/lib/clacky/web/app.js +30 -7
  27. data/lib/clacky/web/components/code-editor.js +197 -0
  28. data/lib/clacky/web/{notify.js → components/notify.js} +1 -1
  29. data/lib/clacky/web/core/aside.js +112 -0
  30. data/lib/clacky/web/core/ext.js +387 -0
  31. data/lib/clacky/web/features/backup/store.js +92 -0
  32. data/lib/clacky/web/features/backup/view.js +94 -0
  33. data/lib/clacky/web/features/billing/store.js +163 -0
  34. data/lib/clacky/web/{billing.js → features/billing/view.js} +132 -240
  35. data/lib/clacky/web/features/brand/store.js +110 -0
  36. data/lib/clacky/web/{brand.js → features/brand/view.js} +49 -199
  37. data/lib/clacky/web/features/channels/store.js +103 -0
  38. data/lib/clacky/web/{channels.js → features/channels/view.js} +50 -127
  39. data/lib/clacky/web/features/creator/store.js +81 -0
  40. data/lib/clacky/web/{creator.js → features/creator/view.js} +53 -102
  41. data/lib/clacky/web/features/mcp/store.js +158 -0
  42. data/lib/clacky/web/{mcp.js → features/mcp/view.js} +57 -134
  43. data/lib/clacky/web/features/model-tester/store.js +77 -0
  44. data/lib/clacky/web/features/model-tester/view.js +7 -0
  45. data/lib/clacky/web/features/profile/store.js +170 -0
  46. data/lib/clacky/web/{profile.js → features/profile/view.js} +94 -144
  47. data/lib/clacky/web/features/share/store.js +145 -0
  48. data/lib/clacky/web/{share.js → features/share/view.js} +66 -202
  49. data/lib/clacky/web/features/skills/store.js +303 -0
  50. data/lib/clacky/web/features/skills/view.js +550 -0
  51. data/lib/clacky/web/features/tasks/store.js +135 -0
  52. data/lib/clacky/web/features/tasks/view.js +241 -0
  53. data/lib/clacky/web/features/trash/store.js +242 -0
  54. data/lib/clacky/web/{trash.js → features/trash/view.js} +102 -293
  55. data/lib/clacky/web/features/version/store.js +165 -0
  56. data/lib/clacky/web/features/version/view.js +323 -0
  57. data/lib/clacky/web/features/workspace/store.js +99 -0
  58. data/lib/clacky/web/features/workspace/view.js +305 -0
  59. data/lib/clacky/web/i18n.js +56 -6
  60. data/lib/clacky/web/index.html +117 -58
  61. data/lib/clacky/web/sessions.js +221 -25
  62. data/lib/clacky/web/settings.js +118 -22
  63. data/lib/clacky/web/skills.js +3 -863
  64. data/lib/clacky/web/vendor/codemirror/codemirror.min.js +29 -0
  65. data/lib/clacky.rb +1 -0
  66. metadata +45 -20
  67. data/lib/clacky/web/backup.js +0 -119
  68. data/lib/clacky/web/model-tester.js +0 -66
  69. data/lib/clacky/web/tasks.js +0 -373
  70. data/lib/clacky/web/version.js +0 -449
  71. data/lib/clacky/web/workspace.js +0 -316
  72. /data/lib/clacky/web/{notify.mp3 → assets/notify.mp3} +0 -0
  73. /data/lib/clacky/web/{datepicker.js → components/datepicker.js} +0 -0
  74. /data/lib/clacky/web/{onboard.js → components/onboard.js} +0 -0
  75. /data/lib/clacky/web/{sidebar.js → components/sidebar.js} +0 -0
  76. /data/lib/clacky/web/{marked.min.js → vendor/marked/marked.min.js} +0 -0
@@ -1,17 +1,16 @@
1
- // channels.jsChannels panel (Agent-First design)
1
+ // ── Channels · view platform metadata + rendering + DOM wiring ───────────
2
2
  //
3
- // Design principle: no configuration forms here.
4
- // This page shows platform status only. All setup is done via Agent with browser automation.
5
- // "Auto Setup" opens a chat session with /channel-manager pre-filled the Agent will use
6
- // browser automation to complete the entire setup on the platform's web console.
7
- // "Test" runs /channel-manager doctor via the Agent and streams results.
8
-
9
- const Channels = (() => {
10
-
11
- // Platform display metadata (use accessor to pick up runtime language)
12
- // SVG sources: dashboard-icons (Lark, multi-color brand mark),
13
- // TDesign icons (WeCom/WeChat, single-color), simpleicons (Discord/Telegram),
14
- // ant-design/ant-design-icons outlined (DingTalk).
3
+ // Owns PLATFORM_META (logos/labels), all card rendering, and event wiring.
4
+ // Reads data through ChannelsStore.state and reacts to store events. Toggle /
5
+ // test / setup go through store actions.
6
+ //
7
+ // Augments the `Channels` facade with onPanelShow.
8
+ //
9
+ // Depends on: ChannelsStore, I18n, global $ helper.
10
+ // ───────────────────────────────────────────────────────────────────────────
11
+
12
+ const ChannelsView = (() => {
13
+
15
14
  function PLATFORM_META() {
16
15
  return {
17
16
  feishu: {
@@ -65,37 +64,12 @@ const Channels = (() => {
65
64
  };
66
65
  }
67
66
 
68
- // ── Public API ──────────────────────────────────────────────────────────────
69
-
70
- async function onPanelShow() {
71
- await _load();
72
- }
73
-
74
- // ── Data Loading ─────────────────────────────────────────────────────────────
75
-
76
- async function _load({ silent = false } = {}) {
77
- const container = $("channels-list");
78
- if (!container) return;
79
- if (!silent) {
80
- container.innerHTML = `<div class="channel-loading">${I18n.t("channels.loading")}</div>`;
81
- }
82
-
83
- try {
84
- const res = await fetch("/api/channels");
85
- const data = await res.json();
86
- _render(data.channels || []);
87
- } catch (e) {
88
- container.innerHTML = `<div class="channel-error">${I18n.t("channels.loadError", { msg: _esc(e.message) })}</div>`;
89
- }
90
- }
91
-
92
- // ── Rendering ─────────────────────────────────────────────────────────────────
93
-
94
- function _render(channels) {
67
+ function _render() {
95
68
  const container = $("channels-list");
96
69
  if (!container) return;
97
70
  container.innerHTML = "";
98
71
 
72
+ const channels = ChannelsStore.state.channels;
99
73
  const meta = PLATFORM_META();
100
74
  const platformIds = Object.keys(meta);
101
75
  const configured = [];
@@ -106,19 +80,8 @@ const Channels = (() => {
106
80
  (serverData.has_config ? configured : unconfigured).push({ pid, serverData, meta: meta[pid] });
107
81
  });
108
82
 
109
- // Connected section
110
- if (configured.length > 0) {
111
- const section = _renderSection("connected", configured);
112
- container.appendChild(section);
113
- }
114
-
115
- // Unconfigured section
116
- if (unconfigured.length > 0) {
117
- const section = _renderSection("unconfigured", unconfigured);
118
- container.appendChild(section);
119
- }
120
-
121
- // Custom adapter development card
83
+ if (configured.length > 0) container.appendChild(_renderSection("connected", configured));
84
+ if (unconfigured.length > 0) container.appendChild(_renderSection("unconfigured", unconfigured));
122
85
  container.appendChild(_renderCustomDevCard());
123
86
  }
124
87
 
@@ -134,8 +97,7 @@ const Channels = (() => {
134
97
  section.appendChild(header);
135
98
 
136
99
  items.forEach(({ pid, serverData, meta }) => {
137
- const card = _renderCard(pid, serverData, meta);
138
- section.appendChild(card);
100
+ section.appendChild(_renderCard(pid, serverData, meta));
139
101
  });
140
102
 
141
103
  return section;
@@ -187,7 +149,6 @@ const Channels = (() => {
187
149
  </div>
188
150
  `;
189
151
 
190
- // Bind events
191
152
  card.querySelector(`#btn-test-${platform}`)?.addEventListener("click", () => _runTest(platform));
192
153
  card.querySelector(`#btn-configure-${platform}`)?.addEventListener("click", () => _openSetup(platform));
193
154
  card.querySelector(`#toggle-${platform}`)?.addEventListener("change", (ev) => _onToggle(platform, ev.target));
@@ -205,35 +166,18 @@ const Channels = (() => {
205
166
  `;
206
167
  }
207
168
 
208
- // ── Status hint helpers ───────────────────────────────────────────────
209
-
210
169
  function _statusHint(enabled, running, hasConfig) {
211
- if (running) {
212
- return `<p class="channel-status-hint hint-ok">✓ ${I18n.t("channels.hint.running")}</p>`;
213
- }
214
- if (enabled) {
215
- return `<p class="channel-status-hint hint-warn">⚠ ${I18n.t("channels.hint.enabledNotRunning")}</p>`;
216
- }
217
- if (hasConfig) {
218
- return `<p class="channel-status-hint hint-idle">${I18n.t("channels.hint.disabled")}</p>`;
219
- }
170
+ if (running) return `<p class="channel-status-hint hint-ok">✓ ${I18n.t("channels.hint.running")}</p>`;
171
+ if (enabled) return `<p class="channel-status-hint hint-warn">⚠ ${I18n.t("channels.hint.enabledNotRunning")}</p>`;
172
+ if (hasConfig) return `<p class="channel-status-hint hint-idle">${I18n.t("channels.hint.disabled")}</p>`;
220
173
  return `<p class="channel-status-hint hint-idle">${I18n.t("channels.hint.notConfigured")}</p>`;
221
174
  }
222
175
 
223
- // ── Toggle handler ───────────────────────────────────────────────────────────
224
-
225
176
  async function _onToggle(platform, checkbox) {
226
177
  const desired = checkbox.checked;
227
178
  checkbox.disabled = true;
228
179
  try {
229
- const res = await fetch(`/api/channels/${encodeURIComponent(platform)}/enabled`, {
230
- method: "PATCH",
231
- headers: { "Content-Type": "application/json" },
232
- body: JSON.stringify({ enabled: desired }),
233
- });
234
- const data = await res.json();
235
- if (!res.ok || !data.ok) throw new Error(data.error || "toggle failed");
236
- await _load({ silent: true });
180
+ await Channels.toggle(platform, desired);
237
181
  } catch (e) {
238
182
  checkbox.checked = !desired;
239
183
  alert("Error: " + e.message);
@@ -242,53 +186,16 @@ const Channels = (() => {
242
186
  }
243
187
  }
244
188
 
245
- // ── Actions ───────────────────────────────────────────────────────────────────
246
-
247
- // Run E2E test: open a session and send /channel-manager doctor
248
- async function _runTest(platform) {
189
+ function _runTest(platform) {
249
190
  const meta = PLATFORM_META()[platform];
250
- await _sendToAgent(meta.testCmd, `Channel E2E Test — ${meta.name}`);
191
+ return Channels.runTest(meta.testCmd, `Channel E2E Test — ${meta.name}`);
251
192
  }
252
193
 
253
- // Open setup: open a session and send /channel-manager setup <platform>
254
- async function _openSetup(platform) {
194
+ function _openSetup(platform) {
255
195
  const meta = PLATFORM_META()[platform];
256
- await _sendToAgent(meta.setupCmd, `Channel Setup — ${meta.name}`);
257
- }
258
-
259
- // Create a session, add it to the list, navigate to it, and send the given command.
260
- // Follows the same pattern as Skills.createInSession().
261
- async function _sendToAgent(command, sessionName) {
262
- try {
263
- // Pick a session name in "Session N" style, consistent with other modules
264
- const maxN = Sessions.all.reduce((max, s) => {
265
- const m = s.name.match(/^Session (\d+)$/);
266
- return m ? Math.max(max, parseInt(m[1], 10)) : max;
267
- }, 0);
268
- const name = sessionName || ("Session " + (maxN + 1));
269
-
270
- const res = await fetch("/api/sessions", {
271
- method: "POST",
272
- headers: { "Content-Type": "application/json" },
273
- body: JSON.stringify({ name, source: "setup" }),
274
- });
275
- const data = await res.json();
276
- if (!res.ok) throw new Error(data.error || I18n.t("channels.sessionError"));
277
- const session = data.session;
278
- if (!session) throw new Error(I18n.t("channels.noSession"));
279
-
280
- // Register in Sessions, refresh sidebar, queue command, then navigate
281
- Sessions.add(session);
282
- Sessions.renderList();
283
- Sessions.setPendingMessage(session.id, command);
284
- Sessions.select(session.id);
285
- } catch (e) {
286
- alert("Error: " + e.message);
287
- }
196
+ return Channels.openSetup(meta.setupCmd, `Channel Setup — ${meta.name}`);
288
197
  }
289
198
 
290
- // ── Custom Adapter Development Card ──────────────────────────────────────────
291
-
292
199
  function _renderCustomDevCard() {
293
200
  const card = document.createElement("div");
294
201
  card.className = "channel-card channel-card-custom-dev";
@@ -320,17 +227,12 @@ const Channels = (() => {
320
227
  `;
321
228
 
322
229
  card.querySelector("#btn-custom-dev-guide")?.addEventListener("click", () => {
323
- _sendToAgent(
324
- I18n.t("channels.customDev.prompt"),
325
- I18n.t("channels.customDev.title")
326
- );
230
+ Channels.sendToAgent(I18n.t("channels.customDev.prompt"), I18n.t("channels.customDev.title"));
327
231
  });
328
232
 
329
233
  return card;
330
234
  }
331
235
 
332
- // ── Helpers ───────────────────────────────────────────────────────────────────
333
-
334
236
  function _esc(str) {
335
237
  return String(str || "")
336
238
  .replace(/&/g, "&amp;")
@@ -339,8 +241,29 @@ const Channels = (() => {
339
241
  .replace(/"/g, "&quot;");
340
242
  }
341
243
 
342
- return {
343
- onPanelShow,
344
- init() {}, // no static DOM to bind; events bound per-render
244
+ function _renderLoading() {
245
+ const container = $("channels-list");
246
+ if (container) container.innerHTML = `<div class="channel-loading">${I18n.t("channels.loading")}</div>`;
247
+ }
248
+
249
+ function _renderError(payload) {
250
+ const container = $("channels-list");
251
+ if (container) container.innerHTML = `<div class="channel-error">${I18n.t("channels.loadError", { msg: _esc(payload.message) })}</div>`;
252
+ }
253
+
254
+ function _subscribe() {
255
+ Channels.on("channels:loading", _renderLoading);
256
+ Channels.on("channels:changed", _render);
257
+ Channels.on("channels:error", _renderError);
258
+ }
259
+
260
+ const viewApi = {
261
+ init() { /* subscriptions wired at load; panel data loads on onPanelShow */ },
262
+ onPanelShow() { return Channels.load(); },
345
263
  };
264
+
265
+ return { init: _subscribe, api: viewApi };
346
266
  })();
267
+
268
+ Object.assign(Channels, ChannelsView.api);
269
+ ChannelsView.init();
@@ -0,0 +1,81 @@
1
+ // ── Creator · store — cloud/local skill data + publish network ─────────────
2
+ //
3
+ // Owns the creator skill lists (cloud / local), loading flag, and the network
4
+ // calls (load catalog, publish a skill). It never renders.
5
+ //
6
+ // Emits store events the view reacts to; mirrors them to the extension bus via
7
+ // Clacky.ext.emit.
8
+ //
9
+ // `Creator` stays the single public facade.
10
+ //
11
+ // Depends on: Clacky.ext.
12
+ // ───────────────────────────────────────────────────────────────────────────
13
+
14
+ const CreatorStore = (() => {
15
+ let _cloudSkills = [];
16
+ let _localSkills = [];
17
+ let _loading = false;
18
+
19
+ const _listeners = {};
20
+
21
+ function _on(event, handler) {
22
+ (_listeners[event] ||= []).push(handler);
23
+ return () => {
24
+ const list = _listeners[event];
25
+ const i = list ? list.indexOf(handler) : -1;
26
+ if (i >= 0) list.splice(i, 1);
27
+ };
28
+ }
29
+
30
+ function _emit(event, payload) {
31
+ (_listeners[event] || []).forEach((h) => h(payload));
32
+ if (window.Clacky && Clacky.ext) Clacky.ext.emit(event, payload);
33
+ }
34
+
35
+ const state = {
36
+ get cloudSkills() { return _cloudSkills; },
37
+ get localSkills() { return _localSkills; },
38
+ get loading() { return _loading; },
39
+ };
40
+
41
+ const Creator = {
42
+ on: _on,
43
+ state,
44
+
45
+ async load() {
46
+ if (_loading) return;
47
+ _loading = true;
48
+ _emit("creator:loading");
49
+ try {
50
+ const res = await fetch("/api/creator/skills");
51
+ const data = await res.json();
52
+ if (!res.ok) throw new Error(data.error || "Load failed");
53
+
54
+ _cloudSkills = data.cloud_skills || [];
55
+ _localSkills = data.local_skills || [];
56
+ _emit("creator:changed", { platformFetchError: data.platform_fetch_error || null });
57
+ } catch (e) {
58
+ console.error("[Creator] load failed", e);
59
+ _emit("creator:error", { message: e.message });
60
+ } finally {
61
+ _loading = false;
62
+ }
63
+ },
64
+
65
+ /** Publish (or update) a skill. Returns { ok, already_exists, error }. */
66
+ async publish(skillName, { force = false } = {}) {
67
+ const url = `/api/my-skills/${encodeURIComponent(skillName)}/publish${force ? "?force=true" : ""}`;
68
+ const res = await fetch(url, { method: "POST" });
69
+ const data = await res.json();
70
+ return {
71
+ ok: res.ok && !!data.ok,
72
+ already_exists: !!data.already_exists,
73
+ error: data.error || null,
74
+ };
75
+ },
76
+ };
77
+
78
+ return Creator;
79
+ })();
80
+
81
+ const Creator = CreatorStore;
@@ -1,21 +1,16 @@
1
- // creator.js — Creator Hub panel
1
+ // ── Creator · view — Creator Hub rendering + publish UI + DOM wiring ───────
2
2
  //
3
- // Three-section layout:
4
- // 1. Cloud Skills — published to the platform (version, download count)
5
- // 2. Local Skills — local only, or published but with local changes
6
- // 3. Create New — opens a new session with /skill-creator
3
+ // Owns rendering of cloud/local skill cards, the publish progress UI (animation
4
+ // + overwrite confirm), the new-skill entry, and sidebar/panel visibility.
5
+ // Reads through CreatorStore.state; load/publish go through store actions.
7
6
  //
8
- // Only visible when Brand.userLicensed is true.
9
- // Load order: after brand.js, before app.js
10
-
11
- const Creator = (() => {
12
- // ── Private state ────────────────────────────────────────────────────
13
- let _cloudSkills = [];
14
- let _localSkills = [];
15
- let _loading = false;
16
- let _domWired = false;
7
+ // Augments the `Creator` facade with onPanelShow and updateSidebarVisibility.
8
+ //
9
+ // Depends on: CreatorStore, I18n, Brand, Skills, Modal.
10
+ // ───────────────────────────────────────────────────────────────────────────
17
11
 
18
- // ── Helpers ──────────────────────────────────────────────────────────
12
+ const CreatorView = (() => {
13
+ let _domWired = false;
19
14
 
20
15
  function escapeHtml(s) {
21
16
  return String(s ?? "")
@@ -27,46 +22,7 @@ const Creator = (() => {
27
22
  return I18n.t ? I18n.t(key) : key;
28
23
  }
29
24
 
30
- // Returns true if local SKILL.md is newer than the last upload
31
- function _hasLocalChanges(skill) {
32
- if (!skill.local_modified_at || !skill.uploaded_at) return false;
33
- try {
34
- return new Date(skill.local_modified_at) > new Date(skill.uploaded_at);
35
- } catch { return false; }
36
- }
37
-
38
- // ── Data loading ─────────────────────────────────────────────────────
39
-
40
- async function _load() {
41
- if (_loading) return;
42
- _loading = true;
43
- _renderLoading();
44
- try {
45
- const res = await fetch("/api/creator/skills");
46
- const data = await res.json();
47
- if (!res.ok) throw new Error(data.error || "Load failed");
48
-
49
- _cloudSkills = data.cloud_skills || [];
50
- _localSkills = data.local_skills || [];
51
- _render();
52
-
53
- if (data.platform_fetch_error) {
54
- _showNotice(
55
- I18n.lang() === "zh"
56
- ? `平台数据加载失败:${data.platform_fetch_error}`
57
- : `Platform data unavailable: ${data.platform_fetch_error}`,
58
- "warn"
59
- );
60
- }
61
- } catch (e) {
62
- console.error("[Creator] load failed", e);
63
- _renderError(e.message);
64
- } finally {
65
- _loading = false;
66
- }
67
- }
68
-
69
- // ── Rendering ────────────────────────────────────────────────────────
25
+ // ── Loading / error ─────────────────────────────────────────────────────
70
26
 
71
27
  function _renderLoading() {
72
28
  const cloudList = document.getElementById("creator-cloud-list");
@@ -76,52 +32,57 @@ const Creator = (() => {
76
32
  if (localList) localList.innerHTML = "";
77
33
  }
78
34
 
79
- function _renderError(msg) {
35
+ function _renderError(payload) {
80
36
  const cloudList = document.getElementById("creator-cloud-list");
81
- if (cloudList) cloudList.innerHTML = `<div class="creator-empty creator-error">${escapeHtml(msg)}</div>`;
37
+ if (cloudList) cloudList.innerHTML = `<div class="creator-empty creator-error">${escapeHtml(payload.message)}</div>`;
82
38
  const localList = document.getElementById("creator-local-list");
83
39
  if (localList) localList.innerHTML = "";
84
40
  }
85
41
 
86
- function _render() {
42
+ function _render(payload) {
87
43
  _renderCloudSection();
88
44
  _renderLocalSection();
89
45
  _wireNewSkillEntry();
46
+
47
+ if (payload && payload.platformFetchError) {
48
+ _showNotice(
49
+ I18n.lang() === "zh"
50
+ ? `平台数据加载失败:${payload.platformFetchError}`
51
+ : `Platform data unavailable: ${payload.platformFetchError}`,
52
+ "warn"
53
+ );
54
+ }
90
55
  }
91
56
 
92
57
  function _renderCloudSection() {
93
- const list = document.getElementById("creator-cloud-list");
94
- const block = document.getElementById("creator-cloud-block");
58
+ const list = document.getElementById("creator-cloud-list");
95
59
  if (!list) return;
96
60
 
97
- if (_cloudSkills.length === 0) {
61
+ const cloudSkills = CreatorStore.state.cloudSkills;
62
+ if (cloudSkills.length === 0) {
98
63
  list.innerHTML = `<div class="creator-empty">${_t("creator.cloud.empty")}</div>`;
99
64
  return;
100
65
  }
101
66
 
102
67
  list.innerHTML = "";
103
- _cloudSkills.forEach(skill => {
104
- list.appendChild(_buildCloudCard(skill));
105
- });
68
+ cloudSkills.forEach(skill => list.appendChild(_buildCloudCard(skill)));
106
69
  }
107
70
 
108
71
  function _renderLocalSection() {
109
- const list = document.getElementById("creator-local-list");
110
- const block = document.getElementById("creator-local-block");
72
+ const list = document.getElementById("creator-local-list");
111
73
  if (!list) return;
112
74
 
113
- if (_localSkills.length === 0) {
75
+ const localSkills = CreatorStore.state.localSkills;
76
+ if (localSkills.length === 0) {
114
77
  list.innerHTML = `<div class="creator-empty">${_t("creator.local.empty")}</div>`;
115
78
  return;
116
79
  }
117
80
 
118
81
  list.innerHTML = "";
119
- _localSkills.forEach(skill => {
120
- list.appendChild(_buildLocalCard(skill));
121
- });
82
+ localSkills.forEach(skill => list.appendChild(_buildLocalCard(skill)));
122
83
  }
123
84
 
124
- // ── Cloud card ───────────────────────────────────────────────────────
85
+ // ── Cloud card ───────────────────────────────────────────────────────────
125
86
 
126
87
  function _buildCloudCard(skill) {
127
88
  const card = document.createElement("div");
@@ -143,15 +104,10 @@ const Creator = (() => {
143
104
  </span>`
144
105
  : "";
145
106
 
146
- // Has local changes indicator
147
107
  const changesHtml = skill.has_local_changes
148
108
  ? `<span class="creator-changes-badge" title="${_t("creator.hasLocalChanges")}">● ${_t("creator.changed")}</span>`
149
109
  : "";
150
110
 
151
- // Action buttons:
152
- // - local_present + has_local_changes → "Update" (publish) + "Iterate" (skill-creator)
153
- // - local_present + no changes → grey disabled "Up to date" + "Iterate"
154
- // - no local copy → nothing (can only iterate to create local first)
155
111
  let actionBtnsHtml = "";
156
112
  if (skill.local_present) {
157
113
  if (skill.has_local_changes) {
@@ -220,15 +176,13 @@ const Creator = (() => {
220
176
 
221
177
  if (skill.local_present) {
222
178
  const iterBtn = card.querySelector(".btn-creator-iterate");
223
- if (iterBtn) {
224
- iterBtn.addEventListener("click", () => _iterateSkill(skill.name));
225
- }
179
+ if (iterBtn) iterBtn.addEventListener("click", () => _iterateSkill(skill.name));
226
180
  }
227
181
 
228
182
  return card;
229
183
  }
230
184
 
231
- // ── Local card ───────────────────────────────────────────────────────
185
+ // ── Local card ─────────────────────────────────────────────────────────
232
186
 
233
187
  function _buildLocalCard(skill) {
234
188
  const card = document.createElement("div");
@@ -277,7 +231,7 @@ const Creator = (() => {
277
231
  return card;
278
232
  }
279
233
 
280
- // ── Publish logic ────────────────────────────────────────────────────
234
+ // ── Publish UI ───────────────────────────────────────────────────────────
281
235
 
282
236
  async function _publishSkill(skillName, publishBtn, progressWrap, progressBar, force, card, isUpdate) {
283
237
  publishBtn.disabled = true;
@@ -300,27 +254,22 @@ const Creator = (() => {
300
254
  let skipFinalReset = false;
301
255
 
302
256
  try {
303
- const url = `/api/my-skills/${encodeURIComponent(skillName)}/publish${force ? "?force=true" : ""}`;
304
- const res = await fetch(url, { method: "POST" });
305
- const data = await res.json();
306
-
257
+ const result = await Creator.publish(skillName, { force });
307
258
  clearInterval(animInterval);
308
259
 
309
- if (!res.ok || !data.ok) {
310
- alreadyExists = !!data.already_exists;
311
- throw new Error(data.error || "Publish failed");
260
+ if (!result.ok) {
261
+ alreadyExists = result.already_exists;
262
+ throw new Error(result.error || "Publish failed");
312
263
  }
313
264
 
314
- // Success
315
265
  progressBar.style.width = "100%";
316
266
  progressBar.dataset.state = "success";
317
267
  publishBtn.dataset.state = "success";
318
268
  if (btnLabel) btnLabel.textContent = I18n.lang() === "zh" ? "已发布 ✓" : "Published ✓";
319
269
 
320
270
  await new Promise(r => setTimeout(r, 1400));
321
- // Reload to get fresh data
322
- await _load();
323
- skipFinalReset = true; // _load() re-renders, no need to reset manually
271
+ await Creator.load();
272
+ skipFinalReset = true;
324
273
 
325
274
  } catch (e) {
326
275
  clearInterval(animInterval);
@@ -368,14 +317,10 @@ const Creator = (() => {
368
317
  }
369
318
  }
370
319
 
371
- // ── Iterate skill (open skill-creator session for an existing skill) ──
372
-
373
320
  function _iterateSkill(skillName) {
374
321
  Skills.createInSession(`/skill-creator ${I18n.t("creator.iterate.prompt")}${skillName}`);
375
322
  }
376
323
 
377
- // ── Create new skill entry ────────────────────────────────────────────
378
-
379
324
  function _wireNewSkillEntry() {
380
325
  const entry = document.getElementById("creator-new-entry");
381
326
  if (!entry || entry.dataset.wired) return;
@@ -383,8 +328,6 @@ const Creator = (() => {
383
328
  entry.addEventListener("click", () => Skills.createInSession());
384
329
  }
385
330
 
386
- // ── Notice bar ────────────────────────────────────────────────────────
387
-
388
331
  function _showNotice(msg, type = "warn") {
389
332
  const container = document.getElementById("creator-cloud-list");
390
333
  if (!container) return;
@@ -396,9 +339,13 @@ const Creator = (() => {
396
339
  container.prepend(el);
397
340
  }
398
341
 
399
- // ── Public API ────────────────────────────────────────────────────────
342
+ function _subscribe() {
343
+ Creator.on("creator:loading", _renderLoading);
344
+ Creator.on("creator:changed", _render);
345
+ Creator.on("creator:error", _renderError);
346
+ }
400
347
 
401
- return {
348
+ const viewApi = {
402
349
  /** Show/hide the creator section.
403
350
  * Hidden for brand consumer users (branded=true, userLicensed=false).
404
351
  * Visible for creators (userLicensed=true) and unbranded users. */
@@ -415,7 +362,6 @@ const Creator = (() => {
415
362
  _domWired = true;
416
363
  _wireNewSkillEntry();
417
364
  }
418
- // Show promo banner and cloud lock for non-licensed users
419
365
  const licensed = Brand.userLicensed;
420
366
  const banner = document.getElementById("creator-promo-banner");
421
367
  const lock = document.getElementById("creator-cloud-lock");
@@ -423,7 +369,12 @@ const Creator = (() => {
423
369
  if (banner) banner.style.display = licensed ? "none" : "";
424
370
  if (lock) lock.style.display = licensed ? "none" : "";
425
371
  if (list) list.style.display = licensed ? "" : "none";
426
- _load();
372
+ Creator.load();
427
373
  },
428
374
  };
375
+
376
+ return { init: _subscribe, api: viewApi };
429
377
  })();
378
+
379
+ Object.assign(Creator, CreatorView.api);
380
+ CreatorView.init();