openclacky 1.3.1 → 1.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +44 -0
- data/Dockerfile +3 -0
- data/README.md +1 -1
- data/README_JA.md +237 -0
- data/lib/clacky/agent/session_serializer.rb +65 -11
- data/lib/clacky/agent/time_machine.rb +247 -26
- data/lib/clacky/agent.rb +12 -1
- data/lib/clacky/agent_config.rb +14 -2
- data/lib/clacky/brand_config.rb +1 -1
- data/lib/clacky/default_agents/_panels/git/panel.js +201 -0
- data/lib/clacky/default_agents/_panels/time_machine/panel.js +640 -0
- data/lib/clacky/default_agents/coding/profile.yml +3 -0
- data/lib/clacky/default_agents/coding/webui/.gitkeep +0 -0
- data/lib/clacky/default_skills/cron-task-creator/SKILL.md +1 -1
- data/lib/clacky/default_skills/extend-openclacky/SKILL.md +6 -4
- data/lib/clacky/default_skills/media-gen/SKILL.md +30 -6
- data/lib/clacky/media/openai_compat.rb +64 -1
- data/lib/clacky/media/output_dir.rb +43 -0
- data/lib/clacky/message_history.rb +9 -0
- data/lib/clacky/server/channel/channel_manager.rb +26 -0
- data/lib/clacky/server/git_panel.rb +115 -0
- data/lib/clacky/server/http_server.rb +521 -13
- data/lib/clacky/server/server_master.rb +6 -4
- data/lib/clacky/utils/environment_detector.rb +16 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +512 -60
- data/lib/clacky/web/app.js +30 -7
- data/lib/clacky/web/components/code-editor.js +197 -0
- data/lib/clacky/web/{notify.js → components/notify.js} +1 -1
- data/lib/clacky/web/core/aside.js +112 -0
- data/lib/clacky/web/core/ext.js +387 -0
- data/lib/clacky/web/features/backup/store.js +92 -0
- data/lib/clacky/web/features/backup/view.js +94 -0
- data/lib/clacky/web/features/billing/store.js +163 -0
- data/lib/clacky/web/{billing.js → features/billing/view.js} +134 -242
- data/lib/clacky/web/features/brand/store.js +110 -0
- data/lib/clacky/web/{brand.js → features/brand/view.js} +49 -199
- data/lib/clacky/web/features/channels/store.js +103 -0
- data/lib/clacky/web/{channels.js → features/channels/view.js} +50 -127
- data/lib/clacky/web/features/creator/store.js +81 -0
- data/lib/clacky/web/{creator.js → features/creator/view.js} +53 -102
- data/lib/clacky/web/features/mcp/store.js +158 -0
- data/lib/clacky/web/{mcp.js → features/mcp/view.js} +57 -134
- data/lib/clacky/web/features/model-tester/store.js +77 -0
- data/lib/clacky/web/features/model-tester/view.js +7 -0
- data/lib/clacky/web/features/profile/store.js +170 -0
- data/lib/clacky/web/{profile.js → features/profile/view.js} +94 -144
- data/lib/clacky/web/features/share/store.js +145 -0
- data/lib/clacky/web/{share.js → features/share/view.js} +66 -202
- data/lib/clacky/web/features/skills/store.js +303 -0
- data/lib/clacky/web/features/skills/view.js +550 -0
- data/lib/clacky/web/features/tasks/store.js +135 -0
- data/lib/clacky/web/features/tasks/view.js +241 -0
- data/lib/clacky/web/features/trash/store.js +242 -0
- data/lib/clacky/web/{trash.js → features/trash/view.js} +102 -293
- data/lib/clacky/web/features/version/store.js +165 -0
- data/lib/clacky/web/features/version/view.js +323 -0
- data/lib/clacky/web/features/workspace/store.js +99 -0
- data/lib/clacky/web/features/workspace/view.js +305 -0
- data/lib/clacky/web/i18n.js +60 -6
- data/lib/clacky/web/index.html +117 -57
- data/lib/clacky/web/sessions.js +221 -25
- data/lib/clacky/web/settings.js +121 -25
- data/lib/clacky/web/skills.js +3 -821
- data/lib/clacky/web/vendor/codemirror/codemirror.min.js +29 -0
- data/lib/clacky.rb +1 -0
- metadata +45 -20
- data/lib/clacky/web/backup.js +0 -119
- data/lib/clacky/web/model-tester.js +0 -66
- data/lib/clacky/web/tasks.js +0 -365
- data/lib/clacky/web/version.js +0 -449
- data/lib/clacky/web/workspace.js +0 -212
- /data/lib/clacky/web/{notify.mp3 → assets/notify.mp3} +0 -0
- /data/lib/clacky/web/{datepicker.js → components/datepicker.js} +0 -0
- /data/lib/clacky/web/{onboard.js → components/onboard.js} +0 -0
- /data/lib/clacky/web/{sidebar.js → components/sidebar.js} +0 -0
- /data/lib/clacky/web/{marked.min.js → vendor/marked/marked.min.js} +0 -0
data/lib/clacky/web/version.js
DELETED
|
@@ -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
|
-
{ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[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
|
-
})();
|
data/lib/clacky/web/workspace.js
DELETED
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|