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
@@ -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;