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
@@ -116,7 +116,10 @@ const Router = (() => {
116
116
  }
117
117
 
118
118
  // Core: apply a view change. Called both from navigate() and hashchange.
119
- function _apply(view, params = {}) {
119
+ // Async because the "session" case may need to fetch /api/sessions/:id when
120
+ // the target session isn't in the paged sidebar list (search clicks, URL
121
+ // deep links, share links, browser back/forward, notification jumps).
122
+ async function _apply(view, params = {}) {
120
123
  _current = view;
121
124
  _params = params;
122
125
 
@@ -135,6 +138,12 @@ const Router = (() => {
135
138
 
136
139
  _hideAll();
137
140
 
141
+ // Leaving a session view → clear agent scope so agent panels don't linger
142
+ // over non-session views. The session case re-sets it below.
143
+ if (view !== "session" && window.Clacky && Clacky.ext && Clacky.ext.context.agentProfile) {
144
+ Clacky.ext.setContext({ agentProfile: null, sessionId: null });
145
+ }
146
+
138
147
  // Reveal #app on first navigation — ensures the correct view (and language)
139
148
  // is already in place before the user sees anything.
140
149
  // #app covers sidebar + main, so data-i18n elements in the sidebar are also
@@ -150,10 +159,14 @@ const Router = (() => {
150
159
 
151
160
  case "session": {
152
161
  const id = params.id;
153
- const s = Sessions.find(id);
162
+ // findOrFetch falls back to the backend when the session isn't in the
163
+ // sidebar's paged `_sessions` (search results, URL deep links, share
164
+ // links, browser back/forward). On success it caches the row in the
165
+ // local `_extraSessions` pool so subsequent sync `find` calls hit too.
166
+ const s = await Sessions.findOrFetch(id);
154
167
  if (!s) {
155
- // Session not found (e.g. deleted) — fall back to welcome
156
- _apply("welcome");
168
+ // Truly not found (deleted, or never existed) — fall back to welcome.
169
+ await _apply("welcome");
157
170
  return;
158
171
  }
159
172
  _setHash(`session/${id}`);
@@ -163,6 +176,13 @@ const Router = (() => {
163
176
  Sessions.updateInfoBar(s);
164
177
  Sessions._restoreMessagesPublic(id);
165
178
  Sessions._setActiveId(id);
179
+ // Scope agent UI / official panels to this session's agent profile, then
180
+ // re-render every slot so the right panels appear (and a previous
181
+ // agent's panels are cleared).
182
+ if (window.Clacky && Clacky.ext) {
183
+ Clacky.ext.setContext({ agentProfile: s.agent_profile || "general", sessionId: id });
184
+ Clacky.ext.emit("session:agent-changed", { sessionId: id, agentProfile: s.agent_profile || "general" });
185
+ }
166
186
  // Immediately re-attach saved progress UI (timer + spinner) so it appears
167
187
  // instantly without waiting for the async history fetch or WS replay.
168
188
  Sessions._attachProgressUI(id);
@@ -277,7 +297,7 @@ const Router = (() => {
277
297
  return;
278
298
  }
279
299
  const { view, params } = _parseHash(location.hash);
280
- _apply(view, params);
300
+ _apply(view, params).catch(err => console.error("Router._apply failed:", err));
281
301
  });
282
302
 
283
303
  return {
@@ -286,13 +306,16 @@ const Router = (() => {
286
306
 
287
307
  /** Navigate to a view. This is the only way panels should change. */
288
308
  navigate(view, params = {}) {
289
- _apply(view, params);
309
+ // Fire-and-forget: _apply is async (may fetch /api/sessions/:id), but
310
+ // navigate() keeps a sync signature so all existing call sites are
311
+ // unaffected. Errors are logged; UI falls back to welcome on missing id.
312
+ _apply(view, params).catch(err => console.error("Router._apply failed:", err));
290
313
  },
291
314
 
292
315
  /** Restore state from current URL hash (called once on boot after data loads). */
293
316
  restoreFromHash() {
294
317
  const { view, params } = _parseHash(location.hash);
295
- _apply(view, params);
318
+ _apply(view, params).catch(err => console.error("Router._apply failed:", err));
296
319
  },
297
320
  };
298
321
  })();
@@ -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
 
@@ -0,0 +1,112 @@
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 = 280;
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 = true;
35
+ try { open = localStorage.getItem(OPEN_KEY) !== "0"; } catch (_e) { /* ignore */ }
36
+ const empty = slotEmpty();
37
+ aside.classList.toggle("collapsed", !open);
38
+ if (opener) opener.style.display = (!open && !empty) ? "" : "none";
39
+ if (overlay) overlay.classList.toggle("active", open && !empty);
40
+ }
41
+
42
+ function setOpen(open) {
43
+ try { localStorage.setItem(OPEN_KEY, open ? "1" : "0"); } catch (_e) { /* ignore */ }
44
+ applyOpenState();
45
+ }
46
+
47
+ function initResize() {
48
+ const aside = $("session-aside");
49
+ const handle = $("session-aside-resize");
50
+ if (!aside || !handle) return;
51
+
52
+ try {
53
+ const saved = parseFloat(localStorage.getItem(WIDTH_KEY));
54
+ if (saved >= MIN_W && saved <= MAX_W) aside.style.setProperty("--session-aside-width", saved + "px");
55
+ } catch (_e) { /* ignore */ }
56
+
57
+ let dragging = false;
58
+ let startX = 0;
59
+ let startW = 0;
60
+
61
+ handle.addEventListener("mousedown", (e) => {
62
+ e.preventDefault();
63
+ dragging = true;
64
+ startX = e.clientX;
65
+ startW = parseFloat(getComputedStyle(aside).getPropertyValue("--session-aside-width"));
66
+ handle.classList.add("active");
67
+ document.body.style.cursor = "col-resize";
68
+ document.body.style.userSelect = "none";
69
+ });
70
+
71
+ document.addEventListener("mousemove", (e) => {
72
+ if (!dragging) return;
73
+ const dx = startX - e.clientX;
74
+ const w = Math.min(MAX_W, Math.max(MIN_W, startW + dx));
75
+ aside.style.setProperty("--session-aside-width", w + "px");
76
+ });
77
+
78
+ document.addEventListener("mouseup", () => {
79
+ if (!dragging) return;
80
+ dragging = false;
81
+ handle.classList.remove("active");
82
+ document.body.style.cursor = "";
83
+ document.body.style.userSelect = "";
84
+ const w = parseFloat(getComputedStyle(aside).getPropertyValue("--session-aside-width"));
85
+ try { localStorage.setItem(WIDTH_KEY, w); } catch (_e) { /* ignore */ }
86
+ });
87
+ }
88
+
89
+ function init() {
90
+ const collapse = $("btn-aside-collapse");
91
+ const opener = $("btn-aside-open");
92
+ const overlay = $("workspace-overlay");
93
+ if (collapse) collapse.addEventListener("click", () => setOpen(false));
94
+ if (opener) opener.addEventListener("click", () => setOpen(true));
95
+ if (overlay) overlay.addEventListener("click", () => setOpen(false));
96
+ initResize();
97
+ applyOpenState();
98
+
99
+ // Re-evaluate opener visibility whenever the slot content changes (panels
100
+ // re-render on session / agent switch).
101
+ const slot = $("ext-slot-session-aside");
102
+ if (slot && window.MutationObserver) {
103
+ new MutationObserver(() => applyOpenState()).observe(slot, { childList: true });
104
+ }
105
+ }
106
+
107
+ if (document.readyState === "loading") {
108
+ document.addEventListener("DOMContentLoaded", init);
109
+ } else {
110
+ init();
111
+ }
112
+ })();