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.
Files changed (107) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +54 -0
  3. data/Dockerfile +3 -0
  4. data/README.md +1 -1
  5. data/README_JA.md +237 -0
  6. data/docs/rich_ui_guide.md +277 -0
  7. data/docs/rich_ui_refactor_plan.md +396 -0
  8. data/lib/clacky/agent/llm_caller.rb +10 -4
  9. data/lib/clacky/agent/session_serializer.rb +52 -7
  10. data/lib/clacky/agent/time_machine.rb +247 -26
  11. data/lib/clacky/agent.rb +15 -3
  12. data/lib/clacky/api_extension.rb +262 -0
  13. data/lib/clacky/api_extension_loader.rb +156 -0
  14. data/lib/clacky/cli.rb +93 -3
  15. data/lib/clacky/client.rb +38 -13
  16. data/lib/clacky/default_agents/_panels/git/panel.js +201 -0
  17. data/lib/clacky/default_agents/_panels/time_machine/panel.js +640 -0
  18. data/lib/clacky/default_agents/coding/profile.yml +3 -0
  19. data/lib/clacky/default_agents/coding/webui/.gitkeep +0 -0
  20. data/lib/clacky/default_skills/cron-task-creator/SKILL.md +1 -1
  21. data/lib/clacky/default_skills/extend-openclacky/SKILL.md +6 -4
  22. data/lib/clacky/default_skills/media-gen/SKILL.md +37 -10
  23. data/lib/clacky/idle_compression_timer.rb +3 -1
  24. data/lib/clacky/locales/en.rb +26 -0
  25. data/lib/clacky/locales/i18n.rb +26 -0
  26. data/lib/clacky/locales/zh.rb +26 -0
  27. data/lib/clacky/media/openai_compat.rb +64 -1
  28. data/lib/clacky/message_history.rb +9 -0
  29. data/lib/clacky/rich_ui/components/base_component.rb +50 -0
  30. data/lib/clacky/rich_ui/components/dialogs/approval_dialog.rb +142 -0
  31. data/lib/clacky/rich_ui/components/dialogs/config_menu_dialog.rb +106 -0
  32. data/lib/clacky/rich_ui/components/dialogs/form_dialog.rb +128 -0
  33. data/lib/clacky/rich_ui/components/sidebar.rb +119 -0
  34. data/lib/clacky/rich_ui/components/sidebar_panels.rb +134 -0
  35. data/lib/clacky/rich_ui/components/status_view.rb +58 -0
  36. data/lib/clacky/rich_ui/components/thinking_live_view.rb +79 -0
  37. data/lib/clacky/rich_ui/entry_tracker.rb +56 -0
  38. data/lib/clacky/rich_ui/layout_adapter.rb +16 -0
  39. data/lib/clacky/rich_ui/progress_handle_adapter.rb +24 -0
  40. data/lib/clacky/rich_ui/rich_ui_controller.rb +868 -0
  41. data/lib/clacky/rich_ui/shell/rich_agent_shell.rb +184 -0
  42. data/lib/clacky/rich_ui/view_renderer.rb +291 -0
  43. data/lib/clacky/rich_ui.rb +57 -0
  44. data/lib/clacky/rich_ui_controller.rb +3 -1549
  45. data/lib/clacky/server/api_extension_dispatcher.rb +120 -0
  46. data/lib/clacky/server/channel/channel_manager.rb +26 -0
  47. data/lib/clacky/server/git_panel.rb +115 -0
  48. data/lib/clacky/server/http_server.rb +547 -15
  49. data/lib/clacky/server/server_master.rb +6 -4
  50. data/lib/clacky/server/session_registry.rb +1 -1
  51. data/lib/clacky/shell_hook_loader.rb +1 -1
  52. data/lib/clacky/tools/edit.rb +14 -2
  53. data/lib/clacky/ui2/ui_controller.rb +7 -0
  54. data/lib/clacky/version.rb +1 -1
  55. data/lib/clacky/web/app.css +511 -101
  56. data/lib/clacky/web/app.js +95 -14
  57. data/lib/clacky/web/components/code-editor.js +197 -0
  58. data/lib/clacky/web/{notify.js → components/notify.js} +1 -1
  59. data/lib/clacky/web/{onboard.js → components/onboard.js} +18 -2
  60. data/lib/clacky/web/core/aside.js +117 -0
  61. data/lib/clacky/web/core/ext.js +387 -0
  62. data/lib/clacky/web/features/backup/store.js +92 -0
  63. data/lib/clacky/web/features/backup/view.js +94 -0
  64. data/lib/clacky/web/features/billing/store.js +163 -0
  65. data/lib/clacky/web/{billing.js → features/billing/view.js} +132 -240
  66. data/lib/clacky/web/features/brand/store.js +110 -0
  67. data/lib/clacky/web/{brand.js → features/brand/view.js} +49 -199
  68. data/lib/clacky/web/features/channels/store.js +103 -0
  69. data/lib/clacky/web/{channels.js → features/channels/view.js} +50 -127
  70. data/lib/clacky/web/features/creator/store.js +81 -0
  71. data/lib/clacky/web/{creator.js → features/creator/view.js} +53 -102
  72. data/lib/clacky/web/features/mcp/store.js +158 -0
  73. data/lib/clacky/web/{mcp.js → features/mcp/view.js} +57 -134
  74. data/lib/clacky/web/features/model-tester/store.js +77 -0
  75. data/lib/clacky/web/features/model-tester/view.js +7 -0
  76. data/lib/clacky/web/features/profile/store.js +170 -0
  77. data/lib/clacky/web/{profile.js → features/profile/view.js} +94 -144
  78. data/lib/clacky/web/features/share/store.js +145 -0
  79. data/lib/clacky/web/{share.js → features/share/view.js} +66 -202
  80. data/lib/clacky/web/features/skills/store.js +331 -0
  81. data/lib/clacky/web/features/skills/view.js +581 -0
  82. data/lib/clacky/web/features/tasks/store.js +135 -0
  83. data/lib/clacky/web/features/tasks/view.js +241 -0
  84. data/lib/clacky/web/features/trash/store.js +242 -0
  85. data/lib/clacky/web/{trash.js → features/trash/view.js} +102 -293
  86. data/lib/clacky/web/features/version/store.js +165 -0
  87. data/lib/clacky/web/features/version/view.js +323 -0
  88. data/lib/clacky/web/features/workspace/store.js +99 -0
  89. data/lib/clacky/web/features/workspace/view.js +305 -0
  90. data/lib/clacky/web/i18n.js +68 -6
  91. data/lib/clacky/web/index.html +113 -62
  92. data/lib/clacky/web/sessions.js +493 -39
  93. data/lib/clacky/web/settings.js +143 -49
  94. data/lib/clacky/web/skills.js +3 -863
  95. data/lib/clacky/web/vendor/codemirror/codemirror.min.js +29 -0
  96. data/lib/clacky/web/ws-dispatcher.js +7 -3
  97. data/lib/clacky.rb +17 -1
  98. metadata +81 -20
  99. data/lib/clacky/web/backup.js +0 -119
  100. data/lib/clacky/web/model-tester.js +0 -66
  101. data/lib/clacky/web/tasks.js +0 -373
  102. data/lib/clacky/web/version.js +0 -449
  103. data/lib/clacky/web/workspace.js +0 -316
  104. /data/lib/clacky/web/{notify.mp3 → assets/notify.mp3} +0 -0
  105. /data/lib/clacky/web/{datepicker.js → components/datepicker.js} +0 -0
  106. /data/lib/clacky/web/{sidebar.js → components/sidebar.js} +0 -0
  107. /data/lib/clacky/web/{marked.min.js → vendor/marked/marked.min.js} +0 -0
@@ -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
- function _apply(view, params = {}) {
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
- const s = Sessions.find(id);
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
- // Session not found (e.g. deleted) — fall back to welcome
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(view, params);
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-overlay").style.display = "flex";
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
- $("modal-overlay").style.display = "none";
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
- $("prompt-modal-overlay").style.display = "flex";
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
- $("rename-modal-overlay").style.display = "flex";
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}">&times;</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);
@@ -28,7 +28,7 @@
28
28
  // ─────────────────────────────────────────────────────────────────────────
29
29
  const Notify = (() => {
30
30
  const STORAGE_KEY = "clacky-notify-sound";
31
- const AUDIO_SRC = "/notify.mp3";
31
+ const AUDIO_SRC = "/assets/notify.mp3";
32
32
 
33
33
  let _audio = null;
34
34
 
@@ -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
- // Back to Step 1
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
- _showSetupStep("lang");
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
+ })();