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.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +44 -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 +65 -11
  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/brand_config.rb +1 -1
  11. data/lib/clacky/default_agents/_panels/git/panel.js +201 -0
  12. data/lib/clacky/default_agents/_panels/time_machine/panel.js +640 -0
  13. data/lib/clacky/default_agents/coding/profile.yml +3 -0
  14. data/lib/clacky/default_agents/coding/webui/.gitkeep +0 -0
  15. data/lib/clacky/default_skills/cron-task-creator/SKILL.md +1 -1
  16. data/lib/clacky/default_skills/extend-openclacky/SKILL.md +6 -4
  17. data/lib/clacky/default_skills/media-gen/SKILL.md +30 -6
  18. data/lib/clacky/media/openai_compat.rb +64 -1
  19. data/lib/clacky/media/output_dir.rb +43 -0
  20. data/lib/clacky/message_history.rb +9 -0
  21. data/lib/clacky/server/channel/channel_manager.rb +26 -0
  22. data/lib/clacky/server/git_panel.rb +115 -0
  23. data/lib/clacky/server/http_server.rb +521 -13
  24. data/lib/clacky/server/server_master.rb +6 -4
  25. data/lib/clacky/utils/environment_detector.rb +16 -0
  26. data/lib/clacky/version.rb +1 -1
  27. data/lib/clacky/web/app.css +512 -60
  28. data/lib/clacky/web/app.js +30 -7
  29. data/lib/clacky/web/components/code-editor.js +197 -0
  30. data/lib/clacky/web/{notify.js → components/notify.js} +1 -1
  31. data/lib/clacky/web/core/aside.js +112 -0
  32. data/lib/clacky/web/core/ext.js +387 -0
  33. data/lib/clacky/web/features/backup/store.js +92 -0
  34. data/lib/clacky/web/features/backup/view.js +94 -0
  35. data/lib/clacky/web/features/billing/store.js +163 -0
  36. data/lib/clacky/web/{billing.js → features/billing/view.js} +134 -242
  37. data/lib/clacky/web/features/brand/store.js +110 -0
  38. data/lib/clacky/web/{brand.js → features/brand/view.js} +49 -199
  39. data/lib/clacky/web/features/channels/store.js +103 -0
  40. data/lib/clacky/web/{channels.js → features/channels/view.js} +50 -127
  41. data/lib/clacky/web/features/creator/store.js +81 -0
  42. data/lib/clacky/web/{creator.js → features/creator/view.js} +53 -102
  43. data/lib/clacky/web/features/mcp/store.js +158 -0
  44. data/lib/clacky/web/{mcp.js → features/mcp/view.js} +57 -134
  45. data/lib/clacky/web/features/model-tester/store.js +77 -0
  46. data/lib/clacky/web/features/model-tester/view.js +7 -0
  47. data/lib/clacky/web/features/profile/store.js +170 -0
  48. data/lib/clacky/web/{profile.js → features/profile/view.js} +94 -144
  49. data/lib/clacky/web/features/share/store.js +145 -0
  50. data/lib/clacky/web/{share.js → features/share/view.js} +66 -202
  51. data/lib/clacky/web/features/skills/store.js +303 -0
  52. data/lib/clacky/web/features/skills/view.js +550 -0
  53. data/lib/clacky/web/features/tasks/store.js +135 -0
  54. data/lib/clacky/web/features/tasks/view.js +241 -0
  55. data/lib/clacky/web/features/trash/store.js +242 -0
  56. data/lib/clacky/web/{trash.js → features/trash/view.js} +102 -293
  57. data/lib/clacky/web/features/version/store.js +165 -0
  58. data/lib/clacky/web/features/version/view.js +323 -0
  59. data/lib/clacky/web/features/workspace/store.js +99 -0
  60. data/lib/clacky/web/features/workspace/view.js +305 -0
  61. data/lib/clacky/web/i18n.js +60 -6
  62. data/lib/clacky/web/index.html +117 -57
  63. data/lib/clacky/web/sessions.js +221 -25
  64. data/lib/clacky/web/settings.js +121 -25
  65. data/lib/clacky/web/skills.js +3 -821
  66. data/lib/clacky/web/vendor/codemirror/codemirror.min.js +29 -0
  67. data/lib/clacky.rb +1 -0
  68. metadata +45 -20
  69. data/lib/clacky/web/backup.js +0 -119
  70. data/lib/clacky/web/model-tester.js +0 -66
  71. data/lib/clacky/web/tasks.js +0 -365
  72. data/lib/clacky/web/version.js +0 -449
  73. data/lib/clacky/web/workspace.js +0 -212
  74. /data/lib/clacky/web/{notify.mp3 → assets/notify.mp3} +0 -0
  75. /data/lib/clacky/web/{datepicker.js → components/datepicker.js} +0 -0
  76. /data/lib/clacky/web/{onboard.js → components/onboard.js} +0 -0
  77. /data/lib/clacky/web/{sidebar.js → components/sidebar.js} +0 -0
  78. /data/lib/clacky/web/{marked.min.js → vendor/marked/marked.min.js} +0 -0
@@ -0,0 +1,165 @@
1
+ // ── Version · store — version data, upgrade lifecycle, network ────────────
2
+ //
3
+ // Owns the version data and the upgrade/restart lifecycle state machine plus
4
+ // every network call (check, upgrade, restart, reconnect polling) and the WS
5
+ // event ingestion. It never renders — it emits "version:changed" carrying a
6
+ // full state snapshot the view re-renders from.
7
+ //
8
+ // `Version` stays the single public facade (checkVersion).
9
+ //
10
+ // Depends on: WS, Clacky.ext.
11
+ // ───────────────────────────────────────────────────────────────────────────
12
+
13
+ const VersionStore = (() => {
14
+ let _current = null;
15
+ let _latest = null;
16
+ let _needsUpdate = false;
17
+ let _upgrading = false;
18
+ let _needsRestart = false;
19
+ let _reconnecting = false;
20
+ let _upgradeDone = false;
21
+ let _restartFailed = false;
22
+ let _logLines = [];
23
+ let _cliCommand = "openclacky";
24
+
25
+ let _reconnectTimer = null;
26
+ let _reconnectDeadline = 0;
27
+ const RECONNECT_TIMEOUT_MS = 30_000;
28
+
29
+ const _listeners = {};
30
+
31
+ function _on(event, handler) {
32
+ (_listeners[event] ||= []).push(handler);
33
+ return () => {
34
+ const list = _listeners[event];
35
+ const i = list ? list.indexOf(handler) : -1;
36
+ if (i >= 0) list.splice(i, 1);
37
+ };
38
+ }
39
+
40
+ function snapshot() {
41
+ return {
42
+ current: _current,
43
+ latest: _latest,
44
+ needsUpdate: _needsUpdate,
45
+ upgrading: _upgrading,
46
+ needsRestart: _needsRestart,
47
+ reconnecting: _reconnecting,
48
+ upgradeDone: _upgradeDone,
49
+ restartFailed: _restartFailed,
50
+ logLines: _logLines.slice(),
51
+ cliCommand: _cliCommand,
52
+ };
53
+ }
54
+
55
+ function _emit(extra) {
56
+ const payload = Object.assign(snapshot(), extra || {});
57
+ (_listeners["version:changed"] || []).forEach((h) => h(payload));
58
+ if (window.Clacky && Clacky.ext) Clacky.ext.emit("version:changed", payload);
59
+ }
60
+
61
+ async function checkVersion() {
62
+ try {
63
+ const res = await fetch("/api/version");
64
+ if (!res.ok) return;
65
+ const data = await res.json();
66
+ _current = data.current;
67
+ _latest = data.latest;
68
+ _needsUpdate = !!data.needs_update;
69
+ if (data.cli_command) _cliCommand = data.cli_command;
70
+ _emit();
71
+ } catch (e) {
72
+ console.warn("[Version] check failed:", e);
73
+ }
74
+ }
75
+
76
+ async function startUpgrade() {
77
+ if (_upgrading || _upgradeDone) return;
78
+ _upgrading = true;
79
+ _logLines = [];
80
+ _emit({ reason: "upgrade-start" });
81
+ try {
82
+ await fetch("/api/version/upgrade", { method: "POST" });
83
+ } catch (e) {
84
+ console.warn("[Version] upgrade request failed:", e);
85
+ _upgrading = false;
86
+ _emit();
87
+ }
88
+ }
89
+
90
+ function startRestart() {
91
+ _reconnecting = true;
92
+ _emit({ reason: "restart-start" });
93
+ try {
94
+ fetch("/api/restart", { method: "POST" }).catch(() => {});
95
+ } catch (_) {}
96
+ _waitForReconnect();
97
+ }
98
+
99
+ function _waitForReconnect() {
100
+ if (_reconnectTimer) clearInterval(_reconnectTimer);
101
+ _reconnectDeadline = Date.now() + RECONNECT_TIMEOUT_MS;
102
+ setTimeout(() => {
103
+ _reconnectTimer = setInterval(async () => {
104
+ if (Date.now() > _reconnectDeadline) {
105
+ clearInterval(_reconnectTimer);
106
+ _reconnectTimer = null;
107
+ _reconnecting = false;
108
+ _restartFailed = true;
109
+ _emit({ reason: "restart-failed" });
110
+ return;
111
+ }
112
+ try {
113
+ const res = await fetch("/api/version", { cache: "no-store" });
114
+ if (res.ok) {
115
+ clearInterval(_reconnectTimer);
116
+ _reconnectTimer = null;
117
+ _reconnecting = false;
118
+ _needsRestart = false;
119
+ _upgradeDone = true;
120
+ _emit({ reason: "reconnected" });
121
+ }
122
+ } catch (_) { /* server not yet up */ }
123
+ }, 2000);
124
+ }, 2500);
125
+ }
126
+
127
+ function retryReconnect() {
128
+ _restartFailed = false;
129
+ _reconnecting = true;
130
+ _emit({ reason: "retry-reconnect" });
131
+ _waitForReconnect();
132
+ }
133
+
134
+ function _handleWsEvent(event) {
135
+ if (event.type === "upgrade_log") {
136
+ _logLines.push(event.line || "");
137
+ _emit({ reason: "log", line: event.line || "" });
138
+ } else if (event.type === "upgrade_complete") {
139
+ _upgrading = false;
140
+ if (event.success) {
141
+ _needsUpdate = false;
142
+ _needsRestart = true;
143
+ _upgradeDone = false;
144
+ }
145
+ _emit({ reason: "upgrade-complete", success: !!event.success });
146
+ }
147
+ }
148
+
149
+ const Version = {
150
+ on: _on,
151
+ snapshot,
152
+ checkVersion,
153
+ startUpgrade,
154
+ startRestart,
155
+ retryReconnect,
156
+
157
+ bootWs() {
158
+ if (typeof WS !== "undefined") WS.onEvent(_handleWsEvent);
159
+ },
160
+ };
161
+
162
+ return Version;
163
+ })();
164
+
165
+ const Version = VersionStore;
@@ -0,0 +1,323 @@
1
+ // ── Version · view — badge, upgrade popover, hover lifecycle ──────────────
2
+ //
3
+ // Renders the sidebar version badge and the fixed upgrade popover, driven
4
+ // entirely from VersionStore snapshots. All hover/click/popover lifecycle and
5
+ // DOM lives here; lifecycle transitions and network go through store actions.
6
+ //
7
+ // Badge states: up-to-date / has-update / is-upgrading / needs-restart /
8
+ // upgrade-done. Popover morphs as the store emits version:changed with reason.
9
+ //
10
+ // Depends on: VersionStore, I18n.
11
+ // ───────────────────────────────────────────────────────────────────────────
12
+
13
+ const VersionView = (() => {
14
+ let _popoverOpen = false;
15
+ let _hoverTimer = null;
16
+ let _autoReloaded = false;
17
+
18
+ const $ = id => document.getElementById(id);
19
+ const el = (tag, attrs = {}, ...children) => {
20
+ const e = document.createElement(tag);
21
+ Object.entries(attrs).forEach(([k, v]) => {
22
+ if (k === "className") e.className = v;
23
+ else if (k === "innerHTML") e.innerHTML = v;
24
+ else e.setAttribute(k, v);
25
+ });
26
+ children.forEach(c => c && e.appendChild(typeof c === "string" ? document.createTextNode(c) : c));
27
+ return e;
28
+ };
29
+
30
+ function S() { return Version.snapshot(); }
31
+
32
+ // ── Badge render ───────────────────────────────────────────────────────
33
+ function _renderBadge() {
34
+ const s = S();
35
+ const badge = $("version-badge");
36
+ const text = $("version-text");
37
+ const dot = $("version-update-dot");
38
+ const restartDot = $("version-restart-dot");
39
+ const check = $("version-done-check");
40
+ const spinner = $("version-spinner");
41
+ if (!badge || !text) return;
42
+
43
+ text.textContent = s.current ? `v${s.current}` : "";
44
+
45
+ if (dot) dot.style.display = "none";
46
+ if (restartDot) restartDot.style.display = "none";
47
+ if (check) check.style.display = "none";
48
+ if (spinner) spinner.style.display = "none";
49
+ badge.className = "version-badge";
50
+
51
+ if (s.upgrading) {
52
+ badge.classList.add("is-upgrading");
53
+ badge.title = I18n.t("upgrade.tooltip.upgrading");
54
+ if (spinner) spinner.style.display = "inline-block";
55
+ } else if (s.needsRestart) {
56
+ badge.classList.add("needs-restart");
57
+ badge.title = I18n.t("upgrade.tooltip.needs_restart");
58
+ if (restartDot) restartDot.style.display = "inline-block";
59
+ } else if (s.upgradeDone) {
60
+ badge.classList.add("upgrade-done");
61
+ badge.title = I18n.t("upgrade.tooltip.done");
62
+ if (check) check.style.display = "inline-block";
63
+ } else if (s.needsUpdate) {
64
+ badge.classList.add("has-update");
65
+ badge.title = I18n.t("upgrade.tooltip.new", { latest: s.latest });
66
+ if (dot) dot.style.display = "inline-block";
67
+ } else {
68
+ badge.title = I18n.t("upgrade.tooltip.ok", { current: s.current });
69
+ }
70
+
71
+ badge.style.display = "flex";
72
+ }
73
+
74
+ // ── Popover ──────────────────────────────────────────────────────────
75
+ function _getOrCreatePopover() {
76
+ let pop = $("version-upgrade-popover");
77
+ if (pop) return pop;
78
+ pop = el("div", { id: "version-upgrade-popover", className: "vup" });
79
+ document.body.appendChild(pop);
80
+ return pop;
81
+ }
82
+
83
+ function _positionPopover() {
84
+ const badge = $("version-badge");
85
+ const pop = $("version-upgrade-popover");
86
+ if (!badge || !pop) return;
87
+ const rect = badge.getBoundingClientRect();
88
+ pop.style.left = rect.left + "px";
89
+ pop.style.bottom = (window.innerHeight - rect.top + 8) + "px";
90
+ pop.style.top = "auto";
91
+ }
92
+
93
+ function _renderPopoverFor(pop) {
94
+ const s = S();
95
+ pop.innerHTML = "";
96
+ if (s.restartFailed) _renderRestartFailedState(pop, s);
97
+ else if (s.reconnecting) _renderReconnectState(pop);
98
+ else if (s.upgrading) _renderProgressState(pop, s);
99
+ else if (s.needsRestart) _renderDoneState(pop);
100
+ else if (s.upgradeDone) _renderDoneState(pop);
101
+ else if (s.needsUpdate) _renderConfirmState(pop, s);
102
+ else _renderUpToDateState(pop, s);
103
+ }
104
+
105
+ function _openPopover() {
106
+ if (_popoverOpen) { _positionPopover(); return; }
107
+ _popoverOpen = true;
108
+ const pop = _getOrCreatePopover();
109
+ _renderPopoverFor(pop);
110
+ pop.style.display = "block";
111
+ _positionPopover();
112
+ requestAnimationFrame(() => pop.classList.add("vup--visible"));
113
+ }
114
+
115
+ function _closePopover() {
116
+ const s = S();
117
+ if (s.upgrading || s.reconnecting) return;
118
+ const pop = $("version-upgrade-popover");
119
+ if (!pop) return;
120
+ pop.classList.remove("vup--visible");
121
+ setTimeout(() => {
122
+ pop.style.display = "none";
123
+ _popoverOpen = false;
124
+ }, 180);
125
+ }
126
+
127
+ // ── Popover states ─────────────────────────────────────────────────────
128
+
129
+ function _renderUpToDateState(pop, s) {
130
+ pop.innerHTML = `
131
+ <p class="vup-up-to-date">
132
+ <span class="vup-check-icon">✓</span>
133
+ ${I18n.t("upgrade.tooltip.ok", { current: s.current })}
134
+ </p>
135
+ `;
136
+ setTimeout(() => { if (_popoverOpen) _closePopover(); }, 2000);
137
+ }
138
+
139
+ function _renderConfirmState(pop, s) {
140
+ pop.innerHTML = `
141
+ <p class="vup-desc">${I18n.t("upgrade.desc")}</p>
142
+ <p class="vup-versions">v${s.current} <span class="vup-arrow">→</span> v${s.latest}</p>
143
+ <div class="vup-actions">
144
+ <button id="vup-btn-upgrade" class="vup-btn-primary">${I18n.t("upgrade.btn.upgrade")}</button>
145
+ <button id="vup-btn-cancel" class="vup-btn-cancel">${I18n.t("upgrade.btn.cancel")}</button>
146
+ </div>
147
+ `;
148
+ $("vup-btn-upgrade").addEventListener("click", () => Version.startUpgrade());
149
+ $("vup-btn-cancel").addEventListener("click", _closePopover);
150
+ }
151
+
152
+ function _renderProgressState(pop, s) {
153
+ pop.innerHTML = `
154
+ <div class="vup-progress-header">
155
+ <span class="vup-installing-dot"></span>
156
+ <span class="vup-installing-label">${I18n.t("upgrade.installing")}</span>
157
+ </div>
158
+ <pre id="vup-log" class="vup-log"></pre>
159
+ `;
160
+ const logEl = $("vup-log");
161
+ if (logEl && s.logLines.length) {
162
+ logEl.textContent = s.logLines.join("\n");
163
+ logEl.scrollTop = logEl.scrollHeight;
164
+ }
165
+ }
166
+
167
+ function _renderDoneState(pop) {
168
+ pop.innerHTML = `
169
+ <div class="vup-done-header">
170
+ <span class="vup-done-icon">✓</span>
171
+ <span>${I18n.t("upgrade.done")}</span>
172
+ </div>
173
+ <button id="vup-btn-restart" class="vup-btn-restart">${I18n.t("upgrade.btn.restart")}</button>
174
+ `;
175
+ $("vup-btn-restart").addEventListener("click", () => Version.startRestart());
176
+ }
177
+
178
+ function _renderReconnectState(pop) {
179
+ pop.innerHTML = `
180
+ <div class="vup-reconnect">
181
+ <div class="vup-reconnect-spinner"></div>
182
+ <p class="vup-reconnect-msg">${I18n.t("upgrade.reconnecting")}</p>
183
+ </div>
184
+ `;
185
+ }
186
+
187
+ function _renderRestartFailedState(pop, s) {
188
+ const safeCmd = String(s.cliCommand).replace(/[&<>"']/g, c => (
189
+ { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c]
190
+ ));
191
+ const cmd = `<code class="vup-cmd">${safeCmd} server</code>`;
192
+ pop.innerHTML = `
193
+ <div class="vup-restart-failed">
194
+ <p class="vup-restart-failed-title">⚠ ${I18n.t("upgrade.restart.timeout.title")}</p>
195
+ <p class="vup-restart-failed-desc">${I18n.t("upgrade.restart.timeout.desc")}</p>
196
+ <ul class="vup-restart-failed-options">
197
+ <li>${I18n.t("upgrade.restart.timeout.tray")}</li>
198
+ <li>${I18n.t("upgrade.restart.timeout.cli", { cmd })}</li>
199
+ </ul>
200
+ <div class="vup-actions">
201
+ <button id="vup-btn-retry" class="vup-btn-primary">${I18n.t("upgrade.restart.timeout.retry")}</button>
202
+ </div>
203
+ </div>
204
+ `;
205
+ const retry = $("vup-btn-retry");
206
+ if (retry) retry.addEventListener("click", () => Version.retryReconnect());
207
+ }
208
+
209
+ // ── Store event reaction ─────────────────────────────────────────────
210
+ function _onChanged(s) {
211
+ _renderBadge();
212
+
213
+ const reason = s.reason;
214
+
215
+ if (reason === "log") {
216
+ const logEl = $("vup-log");
217
+ if (logEl) {
218
+ logEl.textContent += (logEl.textContent ? "\n" : "") + (s.line || "");
219
+ logEl.scrollTop = logEl.scrollHeight;
220
+ }
221
+ return;
222
+ }
223
+
224
+ if (reason === "restart-start") {
225
+ const pop = _getOrCreatePopover();
226
+ _renderReconnectState(pop);
227
+ if (!_popoverOpen) {
228
+ _popoverOpen = true;
229
+ pop.style.display = "block";
230
+ _positionPopover();
231
+ requestAnimationFrame(() => pop.classList.add("vup--visible"));
232
+ }
233
+ return;
234
+ }
235
+
236
+ if (reason === "reconnected") {
237
+ _closePopover();
238
+ if (!_autoReloaded) {
239
+ _autoReloaded = true;
240
+ setTimeout(() => window.location.reload(), 800);
241
+ }
242
+ return;
243
+ }
244
+
245
+ const pop = $("version-upgrade-popover");
246
+ if (!pop || !_popoverOpen) return;
247
+
248
+ if (reason === "upgrade-complete") {
249
+ if (s.success) _renderDoneState(pop);
250
+ else pop.innerHTML = `<p class="vup-error">${I18n.t("upgrade.failed")}</p>`;
251
+ return;
252
+ }
253
+ if (reason === "upgrade-start") { _renderProgressState(pop, s); return; }
254
+ if (reason === "restart-failed") { _renderRestartFailedState(pop, s); return; }
255
+ if (reason === "retry-reconnect") { _renderReconnectState(pop); return; }
256
+ }
257
+
258
+ // ── Init ───────────────────────────────────────────────────────────────
259
+ function init() {
260
+ const badge = $("version-badge");
261
+ if (badge) {
262
+ badge.addEventListener("click", e => {
263
+ e.stopPropagation();
264
+ if (S().reconnecting) { if (!_popoverOpen) _openPopover(); return; }
265
+ });
266
+
267
+ badge.addEventListener("mouseenter", () => {
268
+ if (!S().current) return;
269
+ clearTimeout(_hoverTimer);
270
+ _openPopover();
271
+ });
272
+
273
+ badge.addEventListener("mouseleave", () => {
274
+ _hoverTimer = setTimeout(() => {
275
+ const pop = $("version-upgrade-popover");
276
+ if (pop && pop.matches(":hover")) return;
277
+ _closePopover();
278
+ }, 200);
279
+ });
280
+ }
281
+
282
+ document.addEventListener("mouseover", e => {
283
+ const pop = $("version-upgrade-popover");
284
+ if (pop && e.target.closest("#version-upgrade-popover")) {
285
+ clearTimeout(_hoverTimer);
286
+ }
287
+ });
288
+ document.addEventListener("mouseout", e => {
289
+ const pop = $("version-upgrade-popover");
290
+ if (!pop) return;
291
+ if (e.target.closest("#version-upgrade-popover") && !e.relatedTarget?.closest("#version-upgrade-popover") && !e.relatedTarget?.closest("#version-badge")) {
292
+ _hoverTimer = setTimeout(() => _closePopover(), 200);
293
+ }
294
+ });
295
+
296
+ document.addEventListener("click", e => {
297
+ if (!e.target.closest("#version-badge") && !e.target.closest("#version-upgrade-popover")) {
298
+ const s = S();
299
+ if (_popoverOpen && !s.upgrading && !s.reconnecting) _closePopover();
300
+ }
301
+ });
302
+
303
+ window.addEventListener("resize", () => {
304
+ if (_popoverOpen) _positionPopover();
305
+ });
306
+
307
+ Version.bootWs();
308
+ Version.checkVersion();
309
+ }
310
+
311
+ function _boot() {
312
+ Version.on("version:changed", _onChanged);
313
+ if (document.readyState === "loading") {
314
+ document.addEventListener("DOMContentLoaded", init);
315
+ } else {
316
+ init();
317
+ }
318
+ }
319
+
320
+ return { init: _boot };
321
+ })();
322
+
323
+ VersionView.init();
@@ -0,0 +1,99 @@
1
+ // ── Workspace · store — session context + file-tree network ───────────────
2
+ //
3
+ // Owns the active session id / working dir and the network calls: list one
4
+ // directory level, reveal a file in Finder, download a file. It never renders.
5
+ //
6
+ // `Workspace` stays the single public facade.
7
+ //
8
+ // Depends on: Clacky.ext.
9
+ // ───────────────────────────────────────────────────────────────────────────
10
+ "use strict";
11
+
12
+ const WorkspaceStore = (() => {
13
+ let _sessionId = null;
14
+ let _workingDir = null;
15
+
16
+ const _listeners = {};
17
+
18
+ function _on(event, handler) {
19
+ (_listeners[event] ||= []).push(handler);
20
+ return () => {
21
+ const list = _listeners[event];
22
+ const i = list ? list.indexOf(handler) : -1;
23
+ if (i >= 0) list.splice(i, 1);
24
+ };
25
+ }
26
+
27
+ function _emit(event, payload) {
28
+ (_listeners[event] || []).forEach((h) => h(payload));
29
+ if (window.Clacky && Clacky.ext) Clacky.ext.emit(event, payload);
30
+ }
31
+
32
+ function _absPath(relPath) {
33
+ return _workingDir.replace(/\/+$/, "") + "/" + relPath;
34
+ }
35
+
36
+ const state = {
37
+ get sessionId() { return _sessionId; },
38
+ get workingDir() { return _workingDir; },
39
+ hasSession() { return _sessionId != null; },
40
+ };
41
+
42
+ const Workspace = {
43
+ on: _on,
44
+ state,
45
+
46
+ /** Update active session context. Returns { changed, hadSession }. */
47
+ setSession(session) {
48
+ const newId = session ? session.id : null;
49
+ const newDir = session ? session.working_dir : null;
50
+ const hadSession = _sessionId != null;
51
+ const changed = newId !== _sessionId || newDir !== _workingDir;
52
+ _sessionId = newId;
53
+ _workingDir = newDir;
54
+ if (changed) _emit("workspace:sessionChanged", { sessionId: newId });
55
+ return { changed, hadSession };
56
+ },
57
+
58
+ async fetchEntries(relPath) {
59
+ const url = `/api/sessions/${encodeURIComponent(_sessionId)}/files?path=${encodeURIComponent(relPath || "")}`;
60
+ const resp = await fetch(url);
61
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
62
+ const data = await resp.json();
63
+ return data.entries || [];
64
+ },
65
+
66
+ async revealFile(entry) {
67
+ const resp = await fetch("/api/file-action", {
68
+ method: "POST",
69
+ headers: { "Content-Type": "application/json" },
70
+ body: JSON.stringify({ path: _absPath(entry.path), action: "reveal" })
71
+ });
72
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
73
+ },
74
+
75
+ async fetchFileBlob(entry) {
76
+ const resp = await fetch("/api/file-action", {
77
+ method: "POST",
78
+ headers: { "Content-Type": "application/json" },
79
+ body: JSON.stringify({ path: _absPath(entry.path), action: "download" })
80
+ });
81
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
82
+ return resp.blob();
83
+ },
84
+
85
+ async fetchFileText(entry) {
86
+ const resp = await fetch("/api/file-action", {
87
+ method: "POST",
88
+ headers: { "Content-Type": "application/json" },
89
+ body: JSON.stringify({ path: _absPath(entry.path), action: "download" })
90
+ });
91
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
92
+ return resp.text();
93
+ },
94
+ };
95
+
96
+ return Workspace;
97
+ })();
98
+
99
+ const Workspace = WorkspaceStore;