openclacky 1.3.2 → 1.3.4
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 +54 -0
- data/Dockerfile +3 -0
- data/README.md +1 -1
- data/README_JA.md +237 -0
- data/docs/rich_ui_guide.md +277 -0
- data/docs/rich_ui_refactor_plan.md +396 -0
- data/lib/clacky/agent/llm_caller.rb +10 -4
- data/lib/clacky/agent/session_serializer.rb +52 -7
- data/lib/clacky/agent/time_machine.rb +247 -26
- data/lib/clacky/agent.rb +15 -3
- data/lib/clacky/api_extension.rb +262 -0
- data/lib/clacky/api_extension_loader.rb +156 -0
- data/lib/clacky/cli.rb +93 -3
- data/lib/clacky/client.rb +38 -13
- 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 +37 -10
- data/lib/clacky/idle_compression_timer.rb +3 -1
- data/lib/clacky/locales/en.rb +26 -0
- data/lib/clacky/locales/i18n.rb +26 -0
- data/lib/clacky/locales/zh.rb +26 -0
- data/lib/clacky/media/openai_compat.rb +64 -1
- data/lib/clacky/message_history.rb +9 -0
- data/lib/clacky/rich_ui/components/base_component.rb +50 -0
- data/lib/clacky/rich_ui/components/dialogs/approval_dialog.rb +142 -0
- data/lib/clacky/rich_ui/components/dialogs/config_menu_dialog.rb +106 -0
- data/lib/clacky/rich_ui/components/dialogs/form_dialog.rb +128 -0
- data/lib/clacky/rich_ui/components/sidebar.rb +119 -0
- data/lib/clacky/rich_ui/components/sidebar_panels.rb +134 -0
- data/lib/clacky/rich_ui/components/status_view.rb +58 -0
- data/lib/clacky/rich_ui/components/thinking_live_view.rb +79 -0
- data/lib/clacky/rich_ui/entry_tracker.rb +56 -0
- data/lib/clacky/rich_ui/layout_adapter.rb +16 -0
- data/lib/clacky/rich_ui/progress_handle_adapter.rb +24 -0
- data/lib/clacky/rich_ui/rich_ui_controller.rb +868 -0
- data/lib/clacky/rich_ui/shell/rich_agent_shell.rb +184 -0
- data/lib/clacky/rich_ui/view_renderer.rb +291 -0
- data/lib/clacky/rich_ui.rb +57 -0
- data/lib/clacky/rich_ui_controller.rb +3 -1549
- data/lib/clacky/server/api_extension_dispatcher.rb +120 -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 +547 -15
- data/lib/clacky/server/server_master.rb +6 -4
- data/lib/clacky/server/session_registry.rb +1 -1
- data/lib/clacky/shell_hook_loader.rb +1 -1
- data/lib/clacky/tools/edit.rb +14 -2
- data/lib/clacky/ui2/ui_controller.rb +7 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +511 -101
- data/lib/clacky/web/app.js +95 -14
- 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/{onboard.js → components/onboard.js} +18 -2
- data/lib/clacky/web/core/aside.js +117 -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} +132 -240
- 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 +331 -0
- data/lib/clacky/web/features/skills/view.js +581 -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 +68 -6
- data/lib/clacky/web/index.html +113 -62
- data/lib/clacky/web/sessions.js +493 -39
- data/lib/clacky/web/settings.js +143 -49
- data/lib/clacky/web/skills.js +3 -863
- data/lib/clacky/web/vendor/codemirror/codemirror.min.js +29 -0
- data/lib/clacky/web/ws-dispatcher.js +7 -3
- data/lib/clacky.rb +17 -1
- metadata +81 -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 -373
- data/lib/clacky/web/version.js +0 -449
- data/lib/clacky/web/workspace.js +0 -316
- /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/{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/app.js
CHANGED
|
@@ -10,6 +10,14 @@
|
|
|
10
10
|
// ── DOM helper (shared by all modules loaded after this) ──────────────────
|
|
11
11
|
const $ = id => document.getElementById(id);
|
|
12
12
|
|
|
13
|
+
// ── Inject X-Lang header into every fetch request ─────────────────────────
|
|
14
|
+
const _nativeFetch = window.fetch;
|
|
15
|
+
window.fetch = function(input, init = {}) {
|
|
16
|
+
const headers = new Headers(init.headers || {});
|
|
17
|
+
if (!headers.has("X-Lang")) headers.set("X-Lang", I18n.lang());
|
|
18
|
+
return _nativeFetch.call(this, input, { ...init, headers });
|
|
19
|
+
};
|
|
20
|
+
|
|
13
21
|
// ── Utilities (shared) ────────────────────────────────────────────────────
|
|
14
22
|
function escapeHtml(str) {
|
|
15
23
|
return String(str)
|
|
@@ -116,7 +124,10 @@ const Router = (() => {
|
|
|
116
124
|
}
|
|
117
125
|
|
|
118
126
|
// Core: apply a view change. Called both from navigate() and hashchange.
|
|
119
|
-
|
|
127
|
+
// Async because the "session" case may need to fetch /api/sessions/:id when
|
|
128
|
+
// the target session isn't in the paged sidebar list (search clicks, URL
|
|
129
|
+
// deep links, share links, browser back/forward, notification jumps).
|
|
130
|
+
async function _apply(view, params = {}) {
|
|
120
131
|
_current = view;
|
|
121
132
|
_params = params;
|
|
122
133
|
|
|
@@ -135,6 +146,12 @@ const Router = (() => {
|
|
|
135
146
|
|
|
136
147
|
_hideAll();
|
|
137
148
|
|
|
149
|
+
// Leaving a session view → clear agent scope so agent panels don't linger
|
|
150
|
+
// over non-session views. The session case re-sets it below.
|
|
151
|
+
if (view !== "session" && window.Clacky && Clacky.ext && Clacky.ext.context.agentProfile) {
|
|
152
|
+
Clacky.ext.setContext({ agentProfile: null, sessionId: null });
|
|
153
|
+
}
|
|
154
|
+
|
|
138
155
|
// Reveal #app on first navigation — ensures the correct view (and language)
|
|
139
156
|
// is already in place before the user sees anything.
|
|
140
157
|
// #app covers sidebar + main, so data-i18n elements in the sidebar are also
|
|
@@ -150,10 +167,14 @@ const Router = (() => {
|
|
|
150
167
|
|
|
151
168
|
case "session": {
|
|
152
169
|
const id = params.id;
|
|
153
|
-
|
|
170
|
+
// findOrFetch falls back to the backend when the session isn't in the
|
|
171
|
+
// sidebar's paged `_sessions` (search results, URL deep links, share
|
|
172
|
+
// links, browser back/forward). On success it caches the row in the
|
|
173
|
+
// local `_extraSessions` pool so subsequent sync `find` calls hit too.
|
|
174
|
+
const s = await Sessions.findOrFetch(id);
|
|
154
175
|
if (!s) {
|
|
155
|
-
//
|
|
156
|
-
_apply("welcome");
|
|
176
|
+
// Truly not found (deleted, or never existed) — fall back to welcome.
|
|
177
|
+
await _apply("welcome");
|
|
157
178
|
return;
|
|
158
179
|
}
|
|
159
180
|
_setHash(`session/${id}`);
|
|
@@ -163,6 +184,13 @@ const Router = (() => {
|
|
|
163
184
|
Sessions.updateInfoBar(s);
|
|
164
185
|
Sessions._restoreMessagesPublic(id);
|
|
165
186
|
Sessions._setActiveId(id);
|
|
187
|
+
// Scope agent UI / official panels to this session's agent profile, then
|
|
188
|
+
// re-render every slot so the right panels appear (and a previous
|
|
189
|
+
// agent's panels are cleared).
|
|
190
|
+
if (window.Clacky && Clacky.ext) {
|
|
191
|
+
Clacky.ext.setContext({ agentProfile: s.agent_profile || "general", sessionId: id });
|
|
192
|
+
Clacky.ext.emit("session:agent-changed", { sessionId: id, agentProfile: s.agent_profile || "general" });
|
|
193
|
+
}
|
|
166
194
|
// Immediately re-attach saved progress UI (timer + spinner) so it appears
|
|
167
195
|
// instantly without waiting for the async history fetch or WS replay.
|
|
168
196
|
Sessions._attachProgressUI(id);
|
|
@@ -277,7 +305,7 @@ const Router = (() => {
|
|
|
277
305
|
return;
|
|
278
306
|
}
|
|
279
307
|
const { view, params } = _parseHash(location.hash);
|
|
280
|
-
_apply(view, params);
|
|
308
|
+
_apply(view, params).catch(err => console.error("Router._apply failed:", err));
|
|
281
309
|
});
|
|
282
310
|
|
|
283
311
|
return {
|
|
@@ -286,13 +314,16 @@ const Router = (() => {
|
|
|
286
314
|
|
|
287
315
|
/** Navigate to a view. This is the only way panels should change. */
|
|
288
316
|
navigate(view, params = {}) {
|
|
289
|
-
_apply(
|
|
317
|
+
// Fire-and-forget: _apply is async (may fetch /api/sessions/:id), but
|
|
318
|
+
// navigate() keeps a sync signature so all existing call sites are
|
|
319
|
+
// unaffected. Errors are logged; UI falls back to welcome on missing id.
|
|
320
|
+
_apply(view, params).catch(err => console.error("Router._apply failed:", err));
|
|
290
321
|
},
|
|
291
322
|
|
|
292
323
|
/** Restore state from current URL hash (called once on boot after data loads). */
|
|
293
324
|
restoreFromHash() {
|
|
294
325
|
const { view, params } = _parseHash(location.hash);
|
|
295
|
-
_apply(view, params);
|
|
326
|
+
_apply(view, params).catch(err => console.error("Router._apply failed:", err));
|
|
296
327
|
},
|
|
297
328
|
};
|
|
298
329
|
})();
|
|
@@ -302,13 +333,53 @@ const Modal = (() => {
|
|
|
302
333
|
/** Show a yes/no confirmation dialog. Returns a Promise<boolean>. */
|
|
303
334
|
function confirm(message) {
|
|
304
335
|
return new Promise(resolve => {
|
|
336
|
+
const overlay = $("modal-overlay");
|
|
337
|
+
$("modal-message").textContent = message;
|
|
338
|
+
$("modal-skip-label").style.display = "none";
|
|
339
|
+
$("modal-skip-cb").checked = false;
|
|
340
|
+
if (overlay.parentNode !== document.body || overlay.nextSibling) {
|
|
341
|
+
document.body.appendChild(overlay);
|
|
342
|
+
}
|
|
343
|
+
overlay.style.display = "flex";
|
|
344
|
+
|
|
345
|
+
const cleanup = (result) => {
|
|
346
|
+
overlay.style.display = "none";
|
|
347
|
+
$("modal-yes").onclick = null;
|
|
348
|
+
$("modal-no").onclick = null;
|
|
349
|
+
resolve(result);
|
|
350
|
+
};
|
|
351
|
+
$("modal-yes").onclick = () => cleanup(true);
|
|
352
|
+
$("modal-no").onclick = () => cleanup(false);
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Like confirm(), but shows a "don't show again" checkbox.
|
|
358
|
+
* If localStorage[storageKey] === "1", resolves true immediately.
|
|
359
|
+
* Returns Promise<boolean>.
|
|
360
|
+
*/
|
|
361
|
+
function confirmOnce(storageKey, message, skipLabel) {
|
|
362
|
+
if (localStorage.getItem(storageKey) === "1") return Promise.resolve(true);
|
|
363
|
+
|
|
364
|
+
return new Promise(resolve => {
|
|
365
|
+
const overlay = $("modal-overlay");
|
|
305
366
|
$("modal-message").textContent = message;
|
|
306
|
-
$("modal-
|
|
367
|
+
$("modal-skip-text").textContent = skipLabel;
|
|
368
|
+
$("modal-skip-cb").checked = false;
|
|
369
|
+
$("modal-skip-label").style.display = "flex";
|
|
370
|
+
if (overlay.parentNode !== document.body || overlay.nextSibling) {
|
|
371
|
+
document.body.appendChild(overlay);
|
|
372
|
+
}
|
|
373
|
+
overlay.style.display = "flex";
|
|
307
374
|
|
|
308
375
|
const cleanup = (result) => {
|
|
309
|
-
|
|
376
|
+
overlay.style.display = "none";
|
|
377
|
+
$("modal-skip-label").style.display = "none";
|
|
310
378
|
$("modal-yes").onclick = null;
|
|
311
379
|
$("modal-no").onclick = null;
|
|
380
|
+
if (result && $("modal-skip-cb").checked) {
|
|
381
|
+
localStorage.setItem(storageKey, "1");
|
|
382
|
+
}
|
|
312
383
|
resolve(result);
|
|
313
384
|
};
|
|
314
385
|
$("modal-yes").onclick = () => cleanup(true);
|
|
@@ -319,10 +390,15 @@ const Modal = (() => {
|
|
|
319
390
|
/** Show a text input prompt dialog. Returns a Promise<string|null>. */
|
|
320
391
|
function prompt(message, defaultValue = "") {
|
|
321
392
|
return new Promise(resolve => {
|
|
393
|
+
const overlay = $("prompt-modal-overlay");
|
|
322
394
|
$("prompt-modal-message").textContent = message;
|
|
323
395
|
const input = $("prompt-modal-input");
|
|
324
396
|
input.value = defaultValue;
|
|
325
|
-
|
|
397
|
+
// Re-attach to <body> end so it stacks above dynamically-appended overlays.
|
|
398
|
+
if (overlay.parentNode !== document.body || overlay.nextSibling) {
|
|
399
|
+
document.body.appendChild(overlay);
|
|
400
|
+
}
|
|
401
|
+
overlay.style.display = "flex";
|
|
326
402
|
|
|
327
403
|
// Auto-focus and select all text
|
|
328
404
|
setTimeout(() => {
|
|
@@ -352,10 +428,15 @@ const Modal = (() => {
|
|
|
352
428
|
/** Show a rename dialog. Returns a Promise<string|null>. */
|
|
353
429
|
function rename(currentName = "") {
|
|
354
430
|
return new Promise(resolve => {
|
|
431
|
+
const overlay = $("rename-modal-overlay");
|
|
355
432
|
const input = $("rename-modal-input");
|
|
356
433
|
input.value = currentName;
|
|
357
434
|
input.classList.remove("input-error");
|
|
358
|
-
|
|
435
|
+
// Re-attach to <body> end so it stacks above dynamically-appended overlays.
|
|
436
|
+
if (overlay.parentNode !== document.body || overlay.nextSibling) {
|
|
437
|
+
document.body.appendChild(overlay);
|
|
438
|
+
}
|
|
439
|
+
overlay.style.display = "flex";
|
|
359
440
|
|
|
360
441
|
setTimeout(() => {
|
|
361
442
|
input.focus();
|
|
@@ -400,7 +481,7 @@ const Modal = (() => {
|
|
|
400
481
|
});
|
|
401
482
|
}
|
|
402
483
|
|
|
403
|
-
return { confirm, prompt, rename };
|
|
484
|
+
return { confirm, confirmOnce, prompt, rename };
|
|
404
485
|
})();
|
|
405
486
|
|
|
406
487
|
// ── Toast helper ──────────────────────────────────────────────────────────
|
|
@@ -527,7 +608,7 @@ $("sidebar-overlay").addEventListener("click", _closeSidebar);
|
|
|
527
608
|
let startW = 0;
|
|
528
609
|
|
|
529
610
|
// Restore saved width
|
|
530
|
-
const saved = localStorage.getItem("sidebar-width");
|
|
611
|
+
const saved = localStorage.getItem("clacky-sidebar-width");
|
|
531
612
|
if (saved) {
|
|
532
613
|
const w = parseFloat(saved);
|
|
533
614
|
if (w >= MIN_W && w <= MAX_W) {
|
|
@@ -560,7 +641,7 @@ $("sidebar-overlay").addEventListener("click", _closeSidebar);
|
|
|
560
641
|
handle.classList.remove("active");
|
|
561
642
|
document.body.style.cursor = "";
|
|
562
643
|
document.body.style.userSelect = "";
|
|
563
|
-
localStorage.setItem("sidebar-width", _getWidth());
|
|
644
|
+
localStorage.setItem("clacky-sidebar-width", _getWidth());
|
|
564
645
|
});
|
|
565
646
|
})();
|
|
566
647
|
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/* global CM */
|
|
2
|
+
/**
|
|
3
|
+
* CodeEditor — a reusable wrapper around CodeMirror 6.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* CodeEditor.open({
|
|
7
|
+
* content: '# Hello',
|
|
8
|
+
* language: 'markdown',
|
|
9
|
+
* title: 'SKILL.md',
|
|
10
|
+
* readOnly: false,
|
|
11
|
+
* onSave: async (content) => { ... }
|
|
12
|
+
* });
|
|
13
|
+
*/
|
|
14
|
+
;(function(window) {
|
|
15
|
+
"use strict";
|
|
16
|
+
|
|
17
|
+
const LANG_MAP = {
|
|
18
|
+
markdown: () => CM.markdown({ base: CM.markdownLanguage }),
|
|
19
|
+
md: () => CM.markdown({ base: CM.markdownLanguage }),
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const IMAGE_EXTS = new Set(["png","jpg","jpeg","gif","bmp","webp","svg","ico","tiff","tif","avif"]);
|
|
23
|
+
const BINARY_EXTS = new Set(["zip","gz","7z","tar","dmg","pdf","xls","xlsx","doc","docx","exe","rar","ttf","mov","mp4","mp3","db","db3","sqlite","sqlite3","dat","wasm","bin","so","dylib","dll"]);
|
|
24
|
+
|
|
25
|
+
function _fileKind(filename) {
|
|
26
|
+
if (!filename) return "text";
|
|
27
|
+
const ext = filename.split(".").pop().toLowerCase();
|
|
28
|
+
if (IMAGE_EXTS.has(ext)) return "image";
|
|
29
|
+
if (BINARY_EXTS.has(ext)) return "binary";
|
|
30
|
+
return "text";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function _detectLanguage(filename) {
|
|
34
|
+
if (!filename) return "markdown";
|
|
35
|
+
const ext = filename.split(".").pop().toLowerCase();
|
|
36
|
+
const map = { md: "markdown", markdown: "markdown" };
|
|
37
|
+
return map[ext] || "markdown";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function _isDark() {
|
|
41
|
+
return document.documentElement.getAttribute("data-theme") === "dark";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function _buildExtensions(opts) {
|
|
45
|
+
const extensions = [
|
|
46
|
+
CM.lineNumbers(),
|
|
47
|
+
CM.highlightActiveLineGutter(),
|
|
48
|
+
CM.highlightSpecialChars(),
|
|
49
|
+
CM.history(),
|
|
50
|
+
CM.drawSelection(),
|
|
51
|
+
CM.dropCursor(),
|
|
52
|
+
CM.indentOnInput(),
|
|
53
|
+
CM.bracketMatching(),
|
|
54
|
+
CM.rectangularSelection(),
|
|
55
|
+
CM.crosshairCursor(),
|
|
56
|
+
CM.highlightActiveLine(),
|
|
57
|
+
CM.highlightSelectionMatches(),
|
|
58
|
+
CM.keymap.of([
|
|
59
|
+
...CM.defaultKeymap,
|
|
60
|
+
...CM.historyKeymap,
|
|
61
|
+
...CM.searchKeymap,
|
|
62
|
+
...CM.foldKeymap,
|
|
63
|
+
CM.indentWithTab,
|
|
64
|
+
]),
|
|
65
|
+
CM.search(),
|
|
66
|
+
CM.foldGutter(),
|
|
67
|
+
CM.syntaxHighlighting(CM.defaultHighlightStyle, { fallback: true }),
|
|
68
|
+
CM.EditorView.lineWrapping,
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
if (_isDark()) {
|
|
72
|
+
extensions.push(CM.oneDark);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const langFn = LANG_MAP[opts.language || "markdown"];
|
|
76
|
+
if (langFn) extensions.push(langFn());
|
|
77
|
+
|
|
78
|
+
if (opts.readOnly) {
|
|
79
|
+
extensions.push(CM.EditorState.readOnly.of(true));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (opts.onSave) {
|
|
83
|
+
extensions.push(CM.keymap.of([{
|
|
84
|
+
key: "Mod-s",
|
|
85
|
+
run: () => { opts.onSave(opts._getContent()); return true; }
|
|
86
|
+
}]));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return extensions;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function open(opts) {
|
|
93
|
+
const {
|
|
94
|
+
content = "",
|
|
95
|
+
title = "Editor",
|
|
96
|
+
readOnly = false,
|
|
97
|
+
onSave = null,
|
|
98
|
+
onClose = null,
|
|
99
|
+
imageUrl = null,
|
|
100
|
+
} = opts;
|
|
101
|
+
|
|
102
|
+
const kind = opts.kind || (opts.filename ? _fileKind(opts.filename) : "text");
|
|
103
|
+
const language = opts.language || _detectLanguage(opts.filename);
|
|
104
|
+
|
|
105
|
+
let overlay = document.getElementById("code-editor-overlay");
|
|
106
|
+
if (overlay) overlay.remove();
|
|
107
|
+
|
|
108
|
+
overlay = document.createElement("div");
|
|
109
|
+
overlay.id = "code-editor-overlay";
|
|
110
|
+
overlay.className = "modal-overlay";
|
|
111
|
+
|
|
112
|
+
const cancelLabel = I18n.t("modal.cancel");
|
|
113
|
+
const closeLabel = I18n.t("modal.close");
|
|
114
|
+
const saveLabel = I18n.t("modal.save");
|
|
115
|
+
|
|
116
|
+
const isReadOnlyOrImage = readOnly || kind === "image";
|
|
117
|
+
const footerActions = isReadOnlyOrImage
|
|
118
|
+
? `<button class="btn btn-secondary code-editor-cancel">${closeLabel}</button>`
|
|
119
|
+
: `<button class="btn btn-secondary code-editor-cancel">${cancelLabel}</button><button class="btn btn-primary code-editor-save">${saveLabel}</button>`;
|
|
120
|
+
|
|
121
|
+
overlay.innerHTML = `
|
|
122
|
+
<div class="code-editor-modal${kind === "image" ? " code-editor-modal--image" : ""}">
|
|
123
|
+
<div class="code-editor-header">
|
|
124
|
+
<h3 class="code-editor-title"></h3>
|
|
125
|
+
<button class="code-editor-close" title="${closeLabel}">×</button>
|
|
126
|
+
</div>
|
|
127
|
+
<div class="code-editor-body"></div>
|
|
128
|
+
<div class="code-editor-footer">
|
|
129
|
+
<span class="code-editor-status"></span>
|
|
130
|
+
<div class="code-editor-actions">${footerActions}</div>
|
|
131
|
+
</div>
|
|
132
|
+
</div>`;
|
|
133
|
+
|
|
134
|
+
document.body.appendChild(overlay);
|
|
135
|
+
overlay.querySelector(".code-editor-title").textContent = title;
|
|
136
|
+
|
|
137
|
+
const body = overlay.querySelector(".code-editor-body");
|
|
138
|
+
const status = overlay.querySelector(".code-editor-status");
|
|
139
|
+
const closeBtn = overlay.querySelector(".code-editor-close");
|
|
140
|
+
const cancelBtn = overlay.querySelector(".code-editor-cancel");
|
|
141
|
+
const saveBtn = overlay.querySelector(".code-editor-save");
|
|
142
|
+
|
|
143
|
+
function close() {
|
|
144
|
+
overlay.remove();
|
|
145
|
+
if (onClose) onClose();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
closeBtn.addEventListener("click", close);
|
|
149
|
+
if (cancelBtn) cancelBtn.addEventListener("click", close);
|
|
150
|
+
overlay.addEventListener("click", (e) => { if (e.target === overlay) close(); });
|
|
151
|
+
|
|
152
|
+
if (kind === "image") {
|
|
153
|
+
body.classList.add("code-editor-body--image");
|
|
154
|
+
const img = document.createElement("img");
|
|
155
|
+
img.className = "code-editor-img-preview";
|
|
156
|
+
img.alt = title;
|
|
157
|
+
img.src = imageUrl || "";
|
|
158
|
+
body.appendChild(img);
|
|
159
|
+
return { close };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const editorOpts = { language, readOnly, onSave: null, _getContent: null };
|
|
163
|
+
const getContent = () => view.state.doc.toString();
|
|
164
|
+
editorOpts._getContent = getContent;
|
|
165
|
+
editorOpts.onSave = onSave ? () => doSave() : null;
|
|
166
|
+
|
|
167
|
+
const view = new CM.EditorView({
|
|
168
|
+
state: CM.EditorState.create({
|
|
169
|
+
doc: content,
|
|
170
|
+
extensions: _buildExtensions(editorOpts),
|
|
171
|
+
}),
|
|
172
|
+
parent: body,
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
async function doSave() {
|
|
176
|
+
if (!onSave) return;
|
|
177
|
+
if (saveBtn) saveBtn.disabled = true;
|
|
178
|
+
status.textContent = I18n.t("modal.saving");
|
|
179
|
+
status.className = "code-editor-status";
|
|
180
|
+
try {
|
|
181
|
+
await onSave(getContent());
|
|
182
|
+
close();
|
|
183
|
+
} catch (e) {
|
|
184
|
+
status.textContent = e.message || "Save failed";
|
|
185
|
+
status.className = "code-editor-status code-editor-status-error";
|
|
186
|
+
if (saveBtn) saveBtn.disabled = false;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (saveBtn) saveBtn.addEventListener("click", doSave);
|
|
191
|
+
setTimeout(() => view.focus(), 50);
|
|
192
|
+
|
|
193
|
+
return { view, close, getContent };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
window.CodeEditor = { open, fileKind: _fileKind };
|
|
197
|
+
})(window);
|
|
@@ -110,6 +110,11 @@ const Onboard = (() => {
|
|
|
110
110
|
$("setup-phase-key").style.display = step === "key" ? "" : "none";
|
|
111
111
|
$("setup-dot-1").className = "setup-step" + (step === "lang" ? " active" : " done");
|
|
112
112
|
$("setup-dot-2").className = "setup-step" + (step === "key" ? " active" : "");
|
|
113
|
+
if (step === "key") {
|
|
114
|
+
$("setup-device-block").style.display = "";
|
|
115
|
+
$("setup-manual-toggle").style.display = "";
|
|
116
|
+
$("setup-manual-section").style.display = "none";
|
|
117
|
+
}
|
|
113
118
|
}
|
|
114
119
|
|
|
115
120
|
// Step 2 — API key setup
|
|
@@ -367,9 +372,20 @@ const Onboard = (() => {
|
|
|
367
372
|
|
|
368
373
|
$("setup-btn-test").addEventListener("click", _testAndSave);
|
|
369
374
|
|
|
370
|
-
|
|
375
|
+
$("setup-manual-toggle").addEventListener("click", () => {
|
|
376
|
+
$("setup-device-block").style.display = "none";
|
|
377
|
+
$("setup-manual-toggle").style.display = "none";
|
|
378
|
+
$("setup-manual-section").style.display = "";
|
|
379
|
+
});
|
|
380
|
+
|
|
371
381
|
$("setup-btn-back").addEventListener("click", () => {
|
|
372
|
-
|
|
382
|
+
if ($("setup-manual-section").style.display !== "none") {
|
|
383
|
+
$("setup-device-block").style.display = "";
|
|
384
|
+
$("setup-manual-toggle").style.display = "";
|
|
385
|
+
$("setup-manual-section").style.display = "none";
|
|
386
|
+
} else {
|
|
387
|
+
_showSetupStep("lang");
|
|
388
|
+
}
|
|
373
389
|
});
|
|
374
390
|
|
|
375
391
|
_bindDeviceStep();
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// ── Session aside chrome — resize / collapse / opener ─────────────────────
|
|
2
|
+
//
|
|
3
|
+
// Host-owned controls for the right column (#session-aside). The tab bar and
|
|
4
|
+
// bodies inside are rendered by Clacky.ext; this only drives the surrounding
|
|
5
|
+
// chrome so slot re-renders never disturb width or collapse state.
|
|
6
|
+
//
|
|
7
|
+
// - drag #session-aside-resize to change width (persisted)
|
|
8
|
+
// - #btn-aside-collapse hides the column; #btn-aside-open brings it back
|
|
9
|
+
// - when the slot is empty (no panels for this agent) CSS collapses the
|
|
10
|
+
// column on its own; the opener stays hidden in that case
|
|
11
|
+
//
|
|
12
|
+
// Depends on: nothing (loads right after core/ext.js).
|
|
13
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
14
|
+
"use strict";
|
|
15
|
+
|
|
16
|
+
(() => {
|
|
17
|
+
const WIDTH_KEY = "clacky.aside.width";
|
|
18
|
+
const OPEN_KEY = "clacky.aside.open";
|
|
19
|
+
const MIN_W = 256;
|
|
20
|
+
const MAX_W = 720;
|
|
21
|
+
|
|
22
|
+
const $ = (id) => document.getElementById(id);
|
|
23
|
+
|
|
24
|
+
function slotEmpty() {
|
|
25
|
+
const slot = $("ext-slot-session-aside");
|
|
26
|
+
return !slot || slot.childElementCount === 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function applyOpenState() {
|
|
30
|
+
const aside = $("session-aside");
|
|
31
|
+
const opener = $("btn-aside-open");
|
|
32
|
+
const overlay = $("workspace-overlay");
|
|
33
|
+
if (!aside) return;
|
|
34
|
+
let open = false;
|
|
35
|
+
try {
|
|
36
|
+
const stored = localStorage.getItem(OPEN_KEY);
|
|
37
|
+
open = stored === null ? false : stored !== "0";
|
|
38
|
+
} catch (_e) { /* ignore */ }
|
|
39
|
+
const empty = slotEmpty();
|
|
40
|
+
aside.classList.toggle("collapsed", !open);
|
|
41
|
+
if (opener) opener.style.display = (!open && !empty) ? "" : "none";
|
|
42
|
+
if (overlay) overlay.classList.toggle("active", open && !empty);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function setOpen(open) {
|
|
46
|
+
try { localStorage.setItem(OPEN_KEY, open ? "1" : "0"); } catch (_e) { /* ignore */ }
|
|
47
|
+
applyOpenState();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function initResize() {
|
|
51
|
+
const aside = $("session-aside");
|
|
52
|
+
const handle = $("session-aside-resize");
|
|
53
|
+
if (!aside || !handle) return;
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const saved = parseFloat(localStorage.getItem(WIDTH_KEY));
|
|
57
|
+
if (saved >= MIN_W && saved <= MAX_W) aside.style.setProperty("--session-aside-width", saved + "px");
|
|
58
|
+
} catch (_e) { /* ignore */ }
|
|
59
|
+
|
|
60
|
+
let dragging = false;
|
|
61
|
+
let startX = 0;
|
|
62
|
+
let startW = 0;
|
|
63
|
+
|
|
64
|
+
handle.addEventListener("mousedown", (e) => {
|
|
65
|
+
e.preventDefault();
|
|
66
|
+
dragging = true;
|
|
67
|
+
startX = e.clientX;
|
|
68
|
+
startW = parseFloat(getComputedStyle(aside).getPropertyValue("--session-aside-width"));
|
|
69
|
+
handle.classList.add("active");
|
|
70
|
+
aside.style.transition = "none";
|
|
71
|
+
document.body.style.cursor = "col-resize";
|
|
72
|
+
document.body.style.userSelect = "none";
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
document.addEventListener("mousemove", (e) => {
|
|
76
|
+
if (!dragging) return;
|
|
77
|
+
const dx = startX - e.clientX;
|
|
78
|
+
const w = Math.min(MAX_W, Math.max(MIN_W, startW + dx));
|
|
79
|
+
aside.style.setProperty("--session-aside-width", w + "px");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
document.addEventListener("mouseup", () => {
|
|
83
|
+
if (!dragging) return;
|
|
84
|
+
dragging = false;
|
|
85
|
+
handle.classList.remove("active");
|
|
86
|
+
aside.style.transition = "";
|
|
87
|
+
document.body.style.cursor = "";
|
|
88
|
+
document.body.style.userSelect = "";
|
|
89
|
+
const w = parseFloat(getComputedStyle(aside).getPropertyValue("--session-aside-width"));
|
|
90
|
+
try { localStorage.setItem(WIDTH_KEY, w); } catch (_e) { /* ignore */ }
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function init() {
|
|
95
|
+
const collapse = $("btn-aside-collapse");
|
|
96
|
+
const opener = $("btn-aside-open");
|
|
97
|
+
const overlay = $("workspace-overlay");
|
|
98
|
+
if (collapse) collapse.addEventListener("click", () => setOpen(false));
|
|
99
|
+
if (opener) opener.addEventListener("click", () => setOpen(true));
|
|
100
|
+
if (overlay) overlay.addEventListener("click", () => setOpen(false));
|
|
101
|
+
initResize();
|
|
102
|
+
applyOpenState();
|
|
103
|
+
|
|
104
|
+
// Re-evaluate opener visibility whenever the slot content changes (panels
|
|
105
|
+
// re-render on session / agent switch).
|
|
106
|
+
const slot = $("ext-slot-session-aside");
|
|
107
|
+
if (slot && window.MutationObserver) {
|
|
108
|
+
new MutationObserver(() => applyOpenState()).observe(slot, { childList: true });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (document.readyState === "loading") {
|
|
113
|
+
document.addEventListener("DOMContentLoaded", init);
|
|
114
|
+
} else {
|
|
115
|
+
init();
|
|
116
|
+
}
|
|
117
|
+
})();
|