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
@@ -1,449 +0,0 @@
1
- // ── Version — version check and upgrade flow ───────────────────────────────
2
- //
3
- // Badge states:
4
- // (none) → up-to-date, muted version text
5
- // has-update → amber pulsing dot: new version available
6
- // is-upgrading → spinning ring: gem install in progress
7
- // needs-restart → orange bouncing dot: upgrade done, waiting for restart
8
- // upgrade-done → green check: restarted & reconnected successfully
9
- //
10
- // Flow:
11
- // 1. Page load → checkVersion() → badge shows version number
12
- // 2. needs_update: badge shows amber pulsing dot
13
- // 3. Click badge → fixed popover (confirm state)
14
- // 4. Click "Upgrade" → popover → progress state (live log)
15
- // 5. upgrade_complete (success) → badge: needs-restart; popover: restart button
16
- // 6. Click "Restart" → /api/restart → popover: reconnecting spinner
17
- // → poll /api/version until server back → badge: upgrade-done (green ✓) → reload
18
- // ─────────────────────────────────────────────────────────────────────────
19
-
20
- const Version = (() => {
21
- // ── State ──────────────────────────────────────────────────────────────
22
- let _current = null;
23
- let _latest = null;
24
- let _needsUpdate = false;
25
- let _upgrading = false;
26
- let _needsRestart = false; // upgrade done, waiting for restart
27
- let _reconnecting = false; // restart sent, polling for server to come back
28
- let _upgradeDone = false; // restarted and reconnected successfully
29
- let _restartFailed = false; // 30s passed, server still not responding
30
- let _popoverOpen = false;
31
- let _reconnectTimer = null;
32
- let _reconnectDeadline = 0;
33
- let _logLines = [];
34
- let _cliCommand = "openclacky";
35
-
36
- const RECONNECT_TIMEOUT_MS = 30_000;
37
-
38
- // ── DOM helpers ────────────────────────────────────────────────────────
39
- const $ = id => document.getElementById(id);
40
- const el = (tag, attrs = {}, ...children) => {
41
- const e = document.createElement(tag);
42
- Object.entries(attrs).forEach(([k, v]) => {
43
- if (k === "className") e.className = v;
44
- else if (k === "innerHTML") e.innerHTML = v;
45
- else e.setAttribute(k, v);
46
- });
47
- children.forEach(c => c && e.appendChild(typeof c === "string" ? document.createTextNode(c) : c));
48
- return e;
49
- };
50
-
51
- // ── Version check ──────────────────────────────────────────────────────
52
- async function checkVersion() {
53
- try {
54
- const res = await fetch("/api/version");
55
- if (!res.ok) return;
56
- const data = await res.json();
57
- _current = data.current;
58
- _latest = data.latest;
59
- _needsUpdate = !!data.needs_update;
60
- if (data.cli_command) _cliCommand = data.cli_command;
61
- _renderBadge();
62
- } catch (e) {
63
- console.warn("[Version] check failed:", e);
64
- }
65
- }
66
-
67
- // ── Badge render ───────────────────────────────────────────────────────
68
- function _renderBadge() {
69
- const badge = $("version-badge");
70
- const text = $("version-text");
71
- const dot = $("version-update-dot");
72
- const restartDot = $("version-restart-dot");
73
- const check = $("version-done-check");
74
- const spinner = $("version-spinner");
75
- if (!badge || !text) return;
76
-
77
- text.textContent = _current ? `v${_current}` : "";
78
-
79
- // Reset all indicators
80
- if (dot) dot.style.display = "none";
81
- if (restartDot) restartDot.style.display = "none";
82
- if (check) check.style.display = "none";
83
- if (spinner) spinner.style.display = "none";
84
- badge.className = "version-badge";
85
-
86
- if (_upgrading) {
87
- // Spinning ring: gem install running
88
- badge.classList.add("is-upgrading");
89
- badge.title = I18n.t("upgrade.tooltip.upgrading");
90
- if (spinner) spinner.style.display = "inline-block";
91
- } else if (_needsRestart) {
92
- // Orange bouncing dot: upgrade done, please restart
93
- badge.classList.add("needs-restart");
94
- badge.title = I18n.t("upgrade.tooltip.needs_restart");
95
- if (restartDot) restartDot.style.display = "inline-block";
96
- } else if (_upgradeDone) {
97
- // Green check: restarted successfully
98
- badge.classList.add("upgrade-done");
99
- badge.title = I18n.t("upgrade.tooltip.done");
100
- if (check) check.style.display = "inline-block";
101
- } else if (_needsUpdate) {
102
- // Amber pulsing dot: new version available
103
- badge.classList.add("has-update");
104
- badge.title = I18n.t("upgrade.tooltip.new", { latest: _latest });
105
- if (dot) dot.style.display = "inline-block";
106
- } else {
107
- badge.title = I18n.t("upgrade.tooltip.ok", { current: _current });
108
- }
109
-
110
- badge.style.display = "flex";
111
- }
112
-
113
- // ── Popover (fixed, positioned above badge) ────────────────────────────
114
- function _getOrCreatePopover() {
115
- let pop = $("version-upgrade-popover");
116
- if (pop) return pop;
117
-
118
- pop = el("div", { id: "version-upgrade-popover", className: "vup" });
119
- document.body.appendChild(pop);
120
- return pop;
121
- }
122
-
123
- function _positionPopover() {
124
- const badge = $("version-badge");
125
- const pop = $("version-upgrade-popover");
126
- if (!badge || !pop) return;
127
-
128
- const rect = badge.getBoundingClientRect();
129
- // Appear above the badge, right-aligned to sidebar edge
130
- pop.style.left = rect.left + "px";
131
- pop.style.bottom = (window.innerHeight - rect.top + 8) + "px";
132
- pop.style.top = "auto";
133
- }
134
-
135
- function _openPopover() {
136
- if (_popoverOpen) { _positionPopover(); return; }
137
- _popoverOpen = true;
138
-
139
- const pop = _getOrCreatePopover();
140
- pop.innerHTML = "";
141
-
142
- if (_restartFailed) {
143
- _renderRestartFailedState(pop);
144
- } else if (_reconnecting) {
145
- _renderReconnectState(pop);
146
- } else if (_upgrading) {
147
- _renderProgressState(pop);
148
- } else if (_needsRestart) {
149
- _renderDoneState(pop);
150
- } else if (_upgradeDone) {
151
- _renderDoneState(pop);
152
- } else if (_needsUpdate) {
153
- _renderConfirmState(pop);
154
- } else {
155
- _renderUpToDateState(pop);
156
- }
157
-
158
- pop.style.display = "block";
159
- _positionPopover();
160
-
161
- // Animate in
162
- requestAnimationFrame(() => pop.classList.add("vup--visible"));
163
- }
164
-
165
- function _closePopover() {
166
- // Don't allow closing while upgrading or waiting for server to come back
167
- if (_upgrading || _reconnecting) return;
168
- const pop = $("version-upgrade-popover");
169
- if (!pop) return;
170
- pop.classList.remove("vup--visible");
171
- setTimeout(() => {
172
- pop.style.display = "none";
173
- _popoverOpen = false;
174
- }, 180);
175
- }
176
-
177
- // ── Popover states ─────────────────────────────────────────────────────
178
-
179
- /** State 0: already up to date */
180
- function _renderUpToDateState(pop) {
181
- pop.innerHTML = `
182
- <p class="vup-up-to-date">
183
- <span class="vup-check-icon">✓</span>
184
- ${I18n.t("upgrade.tooltip.ok", { current: _current })}
185
- </p>
186
- `;
187
- // Auto-close after 2 s so user doesn't need to click away
188
- setTimeout(() => { if (_popoverOpen) _closePopover(); }, 2000);
189
- }
190
-
191
- /** State 1: confirm upgrade */
192
- function _renderConfirmState(pop) {
193
- pop.innerHTML = `
194
- <p class="vup-desc">${I18n.t("upgrade.desc")}</p>
195
- <p class="vup-versions">v${_current} <span class="vup-arrow">→</span> v${_latest}</p>
196
- <div class="vup-actions">
197
- <button id="vup-btn-upgrade" class="vup-btn-primary">${I18n.t("upgrade.btn.upgrade")}</button>
198
- <button id="vup-btn-cancel" class="vup-btn-cancel">${I18n.t("upgrade.btn.cancel")}</button>
199
- </div>
200
- `;
201
- $("vup-btn-upgrade").addEventListener("click", () => _startUpgrade(pop));
202
- $("vup-btn-cancel").addEventListener("click", _closePopover);
203
- }
204
-
205
- /** State 2: upgrading — show live log */
206
- function _renderProgressState(pop) {
207
- pop.innerHTML = `
208
- <div class="vup-progress-header">
209
- <span class="vup-installing-dot"></span>
210
- <span class="vup-installing-label">${I18n.t("upgrade.installing")}</span>
211
- </div>
212
- <pre id="vup-log" class="vup-log"></pre>
213
- `;
214
- // Replay any logs already received
215
- const logEl = $("vup-log");
216
- if (logEl && _logLines.length) {
217
- logEl.textContent = _logLines.join("\n");
218
- logEl.scrollTop = logEl.scrollHeight;
219
- }
220
- }
221
-
222
- /** State 3: done — show restart button */
223
- function _renderDoneState(pop) {
224
- pop.innerHTML = `
225
- <div class="vup-done-header">
226
- <span class="vup-done-icon">✓</span>
227
- <span>${I18n.t("upgrade.done")}</span>
228
- </div>
229
- <button id="vup-btn-restart" class="vup-btn-restart">${I18n.t("upgrade.btn.restart")}</button>
230
- `;
231
- $("vup-btn-restart").addEventListener("click", _startRestart);
232
- }
233
-
234
- /** State 4: reconnecting after restart */
235
- function _renderReconnectState(pop) {
236
- pop.innerHTML = `
237
- <div class="vup-reconnect">
238
- <div class="vup-reconnect-spinner"></div>
239
- <p class="vup-reconnect-msg">${I18n.t("upgrade.reconnecting")}</p>
240
- </div>
241
- `;
242
- }
243
-
244
- /** State 5: restart timed out — show both recovery paths (tray + CLI) */
245
- function _renderRestartFailedState(pop) {
246
- const safeCmd = String(_cliCommand).replace(/[&<>"']/g, c => (
247
- { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c]
248
- ));
249
- const cmd = `<code class="vup-cmd">${safeCmd} server</code>`;
250
- pop.innerHTML = `
251
- <div class="vup-restart-failed">
252
- <p class="vup-restart-failed-title">⚠ ${I18n.t("upgrade.restart.timeout.title")}</p>
253
- <p class="vup-restart-failed-desc">${I18n.t("upgrade.restart.timeout.desc")}</p>
254
- <ul class="vup-restart-failed-options">
255
- <li>${I18n.t("upgrade.restart.timeout.tray")}</li>
256
- <li>${I18n.t("upgrade.restart.timeout.cli", { cmd })}</li>
257
- </ul>
258
- <div class="vup-actions">
259
- <button id="vup-btn-retry" class="vup-btn-primary">${I18n.t("upgrade.restart.timeout.retry")}</button>
260
- </div>
261
- </div>
262
- `;
263
- const retry = $("vup-btn-retry");
264
- if (retry) retry.addEventListener("click", _retryReconnect);
265
- }
266
-
267
- // ── Upgrade ────────────────────────────────────────────────────────────
268
- async function _startUpgrade(pop) {
269
- if (_upgrading || _upgradeDone) return;
270
- _upgrading = true;
271
- _logLines = [];
272
- _renderBadge();
273
- _renderProgressState(pop);
274
-
275
- try {
276
- await fetch("/api/version/upgrade", { method: "POST" });
277
- } catch (e) {
278
- console.warn("[Version] upgrade request failed:", e);
279
- _upgrading = false;
280
- _renderBadge();
281
- }
282
- }
283
-
284
- // ── Restart ────────────────────────────────────────────────────────────
285
- async function _startRestart() {
286
- _reconnecting = true;
287
-
288
- // Ensure popover is open and showing the reconnect spinner
289
- const pop = _getOrCreatePopover();
290
- _renderReconnectState(pop);
291
- if (!_popoverOpen) {
292
- _popoverOpen = true;
293
- pop.style.display = "block";
294
- _positionPopover();
295
- requestAnimationFrame(() => pop.classList.add("vup--visible"));
296
- }
297
-
298
- try {
299
- fetch("/api/restart", { method: "POST" }).catch(() => {});
300
- } catch (_) {}
301
-
302
- _waitForReconnect();
303
- }
304
-
305
- function _waitForReconnect() {
306
- if (_reconnectTimer) clearInterval(_reconnectTimer);
307
- _reconnectDeadline = Date.now() + RECONNECT_TIMEOUT_MS;
308
- setTimeout(() => {
309
- _reconnectTimer = setInterval(async () => {
310
- if (Date.now() > _reconnectDeadline) {
311
- clearInterval(_reconnectTimer);
312
- _reconnectTimer = null;
313
- _reconnecting = false;
314
- _restartFailed = true;
315
- _renderBadge();
316
- const pop = $("version-upgrade-popover");
317
- if (pop && _popoverOpen) _renderRestartFailedState(pop);
318
- return;
319
- }
320
- try {
321
- const res = await fetch("/api/version", { cache: "no-store" });
322
- if (res.ok) {
323
- clearInterval(_reconnectTimer);
324
- _reconnectTimer = null;
325
- // Server is back — close popover, badge → green check, then reload
326
- _reconnecting = false;
327
- _needsRestart = false;
328
- _upgradeDone = true;
329
- _renderBadge();
330
- _closePopover();
331
- setTimeout(() => window.location.reload(), 800);
332
- }
333
- } catch (_) { /* server not yet up */ }
334
- }, 2000);
335
- }, 2500);
336
- }
337
-
338
- function _retryReconnect() {
339
- _restartFailed = false;
340
- _reconnecting = true;
341
- const pop = $("version-upgrade-popover");
342
- if (pop && _popoverOpen) _renderReconnectState(pop);
343
- _renderBadge();
344
- _waitForReconnect();
345
- }
346
-
347
- // ── WebSocket events ───────────────────────────────────────────────────
348
- function _handleWsEvent(event) {
349
- if (event.type === "upgrade_log") {
350
- const line = event.line || "";
351
- _logLines.push(line);
352
- // Append to live log if popover is open
353
- const logEl = $("vup-log");
354
- if (logEl) {
355
- logEl.textContent += (logEl.textContent ? "\n" : "") + line;
356
- logEl.scrollTop = logEl.scrollHeight;
357
- }
358
- } else if (event.type === "upgrade_complete") {
359
- _upgrading = false;
360
- if (event.success) {
361
- _needsUpdate = false;
362
- _needsRestart = true; // badge: orange bouncing dot
363
- _upgradeDone = false;
364
- }
365
- // On failure, _needsUpdate stays true so badge stays amber
366
- _renderBadge();
367
- // Morph popover to done/error state
368
- const pop = $("version-upgrade-popover");
369
- if (pop && _popoverOpen) {
370
- if (event.success) {
371
- _renderDoneState(pop);
372
- } else {
373
- pop.innerHTML = `<p class="vup-error">${I18n.t("upgrade.failed")}</p>`;
374
- }
375
- }
376
- }
377
- }
378
-
379
- // ── Init ───────────────────────────────────────────────────────────────
380
- let _hoverTimer = null;
381
-
382
- function init() {
383
- const badge = $("version-badge");
384
- if (badge) {
385
- // Click still works (e.g. during reconnect to keep popover visible)
386
- badge.addEventListener("click", e => {
387
- e.stopPropagation();
388
- if (_reconnecting) { if (!_popoverOpen) _openPopover(); return; }
389
- });
390
-
391
- // Hover to open
392
- badge.addEventListener("mouseenter", () => {
393
- if (!_current) return;
394
- clearTimeout(_hoverTimer);
395
- _openPopover();
396
- });
397
-
398
- // Leave badge or popover → close (with small delay so mouse can move to popover)
399
- badge.addEventListener("mouseleave", () => {
400
- _hoverTimer = setTimeout(() => {
401
- const pop = $("version-upgrade-popover");
402
- if (pop && pop.matches(":hover")) return;
403
- _closePopover();
404
- }, 200);
405
- });
406
- }
407
-
408
- // Keep popover open while hovering it; close when leaving
409
- document.addEventListener("mouseover", e => {
410
- const pop = $("version-upgrade-popover");
411
- if (pop && e.target.closest("#version-upgrade-popover")) {
412
- clearTimeout(_hoverTimer);
413
- }
414
- });
415
- document.addEventListener("mouseout", e => {
416
- const pop = $("version-upgrade-popover");
417
- if (!pop) return;
418
- if (e.target.closest("#version-upgrade-popover") && !e.relatedTarget?.closest("#version-upgrade-popover") && !e.relatedTarget?.closest("#version-badge")) {
419
- _hoverTimer = setTimeout(() => _closePopover(), 200);
420
- }
421
- });
422
-
423
- // Click outside still closes (e.g. keyboard users, edge cases)
424
- document.addEventListener("click", e => {
425
- if (!e.target.closest("#version-badge") && !e.target.closest("#version-upgrade-popover")) {
426
- if (_popoverOpen && !_upgrading && !_reconnecting) _closePopover();
427
- }
428
- });
429
-
430
- // Reposition on window resize
431
- window.addEventListener("resize", () => {
432
- if (_popoverOpen) _positionPopover();
433
- });
434
-
435
- if (typeof WS !== "undefined") {
436
- WS.onEvent(_handleWsEvent);
437
- }
438
-
439
- checkVersion();
440
- }
441
-
442
- if (document.readyState === "loading") {
443
- document.addEventListener("DOMContentLoaded", init);
444
- } else {
445
- init();
446
- }
447
-
448
- return { checkVersion };
449
- })();
@@ -1,212 +0,0 @@
1
- // Workspace panel — lazy file tree for the active session's working directory.
2
- // Lists one directory level at a time via GET /api/sessions/:id/files,
3
- // expands/collapses folders in place, and downloads files on click via
4
- // POST /api/file-action.
5
- "use strict";
6
-
7
- const Workspace = (() => {
8
- const STORAGE_KEY = "clacky.workspace.open";
9
-
10
- let _sessionId = null;
11
- let _workingDir = null;
12
- let _open = false;
13
-
14
- const $ = (id) => document.getElementById(id);
15
- const t = (key) => (typeof I18n !== "undefined" ? I18n.t(key) : key);
16
-
17
- const ICON_FOLDER = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>';
18
- const ICON_FILE = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>';
19
- const ICON_CARET = '<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>';
20
-
21
- function formatSize(bytes) {
22
- if (bytes == null) return "";
23
- if (bytes < 1024) return `${bytes} B`;
24
- const units = ["KB", "MB", "GB", "TB"];
25
- let n = bytes / 1024, i = 0;
26
- while (n >= 1024 && i < units.length - 1) { n /= 1024; i++; }
27
- return `${n < 10 ? n.toFixed(1) : Math.round(n)} ${units[i]}`;
28
- }
29
-
30
- async function fetchEntries(relPath) {
31
- const url = `/api/sessions/${encodeURIComponent(_sessionId)}/files?path=${encodeURIComponent(relPath || "")}`;
32
- const resp = await fetch(url);
33
- if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
34
- const data = await resp.json();
35
- return data.entries || [];
36
- }
37
-
38
- function renderEntries(entries) {
39
- const frag = document.createDocumentFragment();
40
- if (!entries.length) {
41
- const empty = document.createElement("div");
42
- empty.className = "wt-empty";
43
- empty.textContent = t("workspace.empty");
44
- frag.appendChild(empty);
45
- return frag;
46
- }
47
- for (const entry of entries) {
48
- frag.appendChild(buildNode(entry));
49
- }
50
- return frag;
51
- }
52
-
53
- function buildNode(entry) {
54
- const node = document.createElement("div");
55
- node.className = "wt-node";
56
-
57
- const row = document.createElement("div");
58
- row.className = "wt-row";
59
- row.title = entry.name;
60
-
61
- const caret = document.createElement("span");
62
- caret.className = "wt-caret" + (entry.type === "dir" ? "" : " leaf");
63
- if (entry.type === "dir") caret.innerHTML = ICON_CARET;
64
-
65
- const icon = document.createElement("span");
66
- icon.className = "wt-icon";
67
- icon.innerHTML = entry.type === "dir" ? ICON_FOLDER : ICON_FILE;
68
-
69
- const name = document.createElement("span");
70
- name.className = "wt-name";
71
- name.textContent = entry.name;
72
-
73
- row.appendChild(caret);
74
- row.appendChild(icon);
75
- row.appendChild(name);
76
-
77
- if (entry.type === "file") {
78
- const size = document.createElement("span");
79
- size.className = "wt-size";
80
- size.textContent = formatSize(entry.size);
81
- row.appendChild(size);
82
- }
83
-
84
- node.appendChild(row);
85
-
86
- if (entry.type === "dir") {
87
- const children = document.createElement("div");
88
- children.className = "wt-children";
89
- children.style.display = "none";
90
- node.appendChild(children);
91
- row.addEventListener("click", () => toggleDir(entry, caret, children));
92
- } else {
93
- row.addEventListener("click", () => downloadFile(entry));
94
- }
95
-
96
- return node;
97
- }
98
-
99
- async function toggleDir(entry, caret, children) {
100
- const isOpen = caret.classList.contains("open");
101
- if (isOpen) {
102
- caret.classList.remove("open");
103
- children.style.display = "none";
104
- return;
105
- }
106
- caret.classList.add("open");
107
- children.style.display = "";
108
- if (children.dataset.loaded === "1") return;
109
-
110
- children.innerHTML = `<div class="wt-loading">${t("workspace.loading")}</div>`;
111
- try {
112
- const entries = await fetchEntries(entry.path);
113
- children.innerHTML = "";
114
- children.appendChild(renderEntries(entries));
115
- children.dataset.loaded = "1";
116
- } catch (err) {
117
- console.error("workspace load failed:", err);
118
- children.innerHTML = `<div class="wt-error">${t("workspace.error")}</div>`;
119
- }
120
- }
121
-
122
- async function downloadFile(entry) {
123
- const fullPath = _workingDir.replace(/\/+$/, "") + "/" + entry.path;
124
- try {
125
- const resp = await fetch("/api/file-action", {
126
- method: "POST",
127
- headers: { "Content-Type": "application/json" },
128
- body: JSON.stringify({ path: fullPath, action: "download" })
129
- });
130
- if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
131
- const blob = await resp.blob();
132
- const url = URL.createObjectURL(blob);
133
- const a = document.createElement("a");
134
- a.href = url;
135
- a.download = entry.name;
136
- document.body.appendChild(a);
137
- a.click();
138
- a.remove();
139
- URL.revokeObjectURL(url);
140
- } catch (err) {
141
- console.error("download failed:", err);
142
- if (typeof Modal !== "undefined") Modal.toast(t("workspace.downloadFailed"), "error");
143
- }
144
- }
145
-
146
- async function loadRoot() {
147
- const tree = $("workspace-tree");
148
- if (!tree || !_sessionId) return;
149
- tree.innerHTML = `<div class="wt-loading">${t("workspace.loading")}</div>`;
150
- try {
151
- const entries = await fetchEntries("");
152
- tree.innerHTML = "";
153
- tree.appendChild(renderEntries(entries));
154
- } catch (err) {
155
- console.error("workspace load failed:", err);
156
- tree.innerHTML = `<div class="wt-error">${t("workspace.error")}</div>`;
157
- }
158
- }
159
-
160
- function applyOpenState() {
161
- const panel = $("workspace-panel");
162
- const opener = $("btn-workspace-open");
163
- if (!panel) return;
164
- const hasSession = !!_sessionId;
165
- panel.classList.toggle("collapsed", !(_open && hasSession));
166
- if (opener) opener.style.display = (!_open && hasSession) ? "" : "none";
167
- }
168
-
169
- function setOpen(open) {
170
- _open = open;
171
- try { localStorage.setItem(STORAGE_KEY, open ? "1" : "0"); } catch (_) {}
172
- applyOpenState();
173
- if (open) loadRoot();
174
- }
175
-
176
- return {
177
- init() {
178
- try { _open = localStorage.getItem(STORAGE_KEY) === "1"; } catch (_) { _open = false; }
179
-
180
- const close = $("btn-workspace-close");
181
- const opener = $("btn-workspace-open");
182
- const refresh = $("btn-workspace-refresh");
183
- if (close) close.addEventListener("click", () => setOpen(false));
184
- if (opener) opener.addEventListener("click", () => setOpen(true));
185
- if (refresh) refresh.addEventListener("click", () => loadRoot());
186
-
187
- applyOpenState();
188
- },
189
-
190
- // Called from Sessions.updateInfoBar whenever the active session changes.
191
- // On a real session switch (from one session to another) we always collapse
192
- // the panel: the file list is only ever loaded when the user explicitly
193
- // expands it (which triggers a single refresh via setOpen), so the list is
194
- // never shown stale across sessions. The first attach (no previous session)
195
- // is not a switch and keeps the restored open state.
196
- onSession(session) {
197
- const newId = session ? session.id : null;
198
- const newDir = session ? session.working_dir : null;
199
- const hadSession = _sessionId != null;
200
- const changed = newId !== _sessionId || newDir !== _workingDir;
201
- _sessionId = newId;
202
- _workingDir = newDir;
203
- if (changed && hadSession && _open) setOpen(false);
204
- applyOpenState();
205
- // First attach with the panel restored open: load once.
206
- if (!hadSession && _open && _sessionId) loadRoot();
207
- }
208
- };
209
- })();
210
-
211
- document.addEventListener("DOMContentLoaded", () => Workspace.init());
212
- window.Workspace = Workspace;
File without changes