openclacky 1.3.6 → 1.3.7

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 (103) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +30 -0
  3. data/lib/clacky/agent.rb +8 -1
  4. data/lib/clacky/agent_profile.rb +121 -53
  5. data/lib/clacky/brand_config.rb +309 -6
  6. data/lib/clacky/cli.rb +4 -57
  7. data/lib/clacky/default_extensions/coding/ext.yml +18 -0
  8. data/lib/clacky/default_extensions/ext-studio/agents/ext-developer/system_prompt.md +95 -0
  9. data/lib/clacky/default_extensions/ext-studio/api/handler.rb +254 -0
  10. data/lib/clacky/default_extensions/ext-studio/ext.yml +32 -0
  11. data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +1269 -0
  12. data/lib/clacky/default_extensions/ext-studio/skills/ext-debug/SKILL.md +71 -0
  13. data/lib/clacky/default_extensions/ext-studio/skills/ext-publish/SKILL.md +74 -0
  14. data/lib/clacky/default_extensions/ext-studio/skills/ext-scaffold/SKILL.md +65 -0
  15. data/lib/clacky/default_extensions/general/ext.yml +18 -0
  16. data/lib/clacky/default_extensions/git/ext.yml +12 -0
  17. data/lib/clacky/{default_agents/_panels/git/panel.js → default_extensions/git/panels/git/view.js} +1 -2
  18. data/lib/clacky/default_extensions/meeting/{handler.rb → api/handler.rb} +78 -26
  19. data/lib/clacky/default_extensions/meeting/ext.yml +15 -0
  20. data/lib/clacky/default_extensions/meeting/{meeting.js → panels/meeting/view.js} +266 -89
  21. data/lib/clacky/default_extensions/meeting/skills/meeting-summarizer/SKILL.md +2 -1
  22. data/lib/clacky/default_extensions/time_machine/ext.yml +12 -0
  23. data/lib/clacky/{default_agents/_panels/time_machine/panel.js → default_extensions/time_machine/panels/time_machine/view.js} +58 -47
  24. data/lib/clacky/default_skills/channel-manager/discord_setup.rb +1 -1
  25. data/lib/clacky/default_skills/extend-openclacky/SKILL.md +83 -18
  26. data/lib/clacky/{api_extension.rb → extension/api_extension.rb} +27 -11
  27. data/lib/clacky/extension/api_loader.rb +136 -0
  28. data/lib/clacky/extension/cli_commands.rb +226 -0
  29. data/lib/clacky/{server/api_extension_dispatcher.rb → extension/dispatcher.rb} +22 -9
  30. data/lib/clacky/extension/hook_loader.rb +77 -0
  31. data/lib/clacky/extension/loader.rb +483 -0
  32. data/lib/clacky/extension/packager.rb +226 -0
  33. data/lib/clacky/{patch_loader.rb → extension/patch_loader.rb} +45 -0
  34. data/lib/clacky/extension/scaffold/template_renderer.rb +64 -0
  35. data/lib/clacky/extension/scaffold/templates/full/README.md.erb +48 -0
  36. data/lib/clacky/extension/scaffold/templates/full/agents/designer.md +5 -0
  37. data/lib/clacky/extension/scaffold/templates/full/api/handler.rb.erb +16 -0
  38. data/lib/clacky/extension/scaffold/templates/full/channels/noop.rb.erb +40 -0
  39. data/lib/clacky/extension/scaffold/templates/full/ext.yml.erb +41 -0
  40. data/lib/clacky/extension/scaffold/templates/full/hooks/audit.rb +9 -0
  41. data/lib/clacky/extension/scaffold/templates/full/panels/dashboard/view.js.erb +194 -0
  42. data/lib/clacky/extension/scaffold/templates/full/patches/audit.rb +15 -0
  43. data/lib/clacky/extension/scaffold/templates/full/skills/__slug__-skill/SKILL.md.erb +8 -0
  44. data/lib/clacky/extension/scaffold/templates/hello/api/handler.rb.erb +9 -0
  45. data/lib/clacky/extension/scaffold/templates/hello/ext.yml.erb +12 -0
  46. data/lib/clacky/extension/scaffold/templates/hello/panels/hello/view.js.erb +31 -0
  47. data/lib/clacky/extension/scaffold.rb +55 -0
  48. data/lib/clacky/extension/verifier.rb +196 -0
  49. data/lib/clacky/identity.rb +70 -0
  50. data/lib/clacky/locales/en.rb +19 -1
  51. data/lib/clacky/locales/zh.rb +19 -1
  52. data/lib/clacky/platform_http_client.rb +18 -15
  53. data/lib/clacky/server/channel/extension_adapter_loader.rb +32 -0
  54. data/lib/clacky/server/channel.rb +6 -0
  55. data/lib/clacky/server/http_server.rb +313 -144
  56. data/lib/clacky/server/session_registry.rb +9 -2
  57. data/lib/clacky/skill_loader.rb +28 -1
  58. data/lib/clacky/ui2/ui_controller.rb +9 -6
  59. data/lib/clacky/version.rb +1 -1
  60. data/lib/clacky/web/app.css +748 -573
  61. data/lib/clacky/web/app.js +63 -15
  62. data/lib/clacky/web/auth.js +2 -0
  63. data/lib/clacky/web/components/notify.js +2 -0
  64. data/lib/clacky/web/components/sidebar.js +1 -5
  65. data/lib/clacky/web/core/ext.js +316 -79
  66. data/lib/clacky/web/features/backup/store.js +1 -0
  67. data/lib/clacky/web/features/backup/view.js +0 -1
  68. data/lib/clacky/web/features/brand/view.js +13 -5
  69. data/lib/clacky/web/features/extensions/store.js +184 -0
  70. data/lib/clacky/web/features/extensions/view.js +389 -0
  71. data/lib/clacky/web/features/new-session/store.js +186 -0
  72. data/lib/clacky/web/features/new-session/view.js +319 -0
  73. data/lib/clacky/web/features/share/view.js +63 -14
  74. data/lib/clacky/web/features/skills/store.js +3 -1
  75. data/lib/clacky/web/features/skills/view.js +1 -1
  76. data/lib/clacky/web/features/tasks/view.js +12 -0
  77. data/lib/clacky/web/features/trash/store.js +24 -6
  78. data/lib/clacky/web/features/trash/view.js +46 -9
  79. data/lib/clacky/web/features/workspace/store.js +2 -0
  80. data/lib/clacky/web/features/workspace/view.js +0 -1
  81. data/lib/clacky/web/i18n.js +118 -106
  82. data/lib/clacky/web/index.html +138 -188
  83. data/lib/clacky/web/sessions.js +64 -269
  84. data/lib/clacky/web/settings.js +1 -1
  85. data/lib/clacky/web/skills.js +2 -0
  86. data/lib/clacky/web/theme.js +2 -0
  87. data/lib/clacky/web/ws-dispatcher.js +1 -0
  88. data/lib/clacky/web/ws.js +2 -0
  89. data/lib/clacky.rb +21 -10
  90. metadata +55 -24
  91. data/benchmark/runner.rb +0 -441
  92. data/lib/clacky/api_extension_loader.rb +0 -168
  93. data/lib/clacky/default_agents/SOUL.md +0 -3
  94. data/lib/clacky/default_agents/USER.md +0 -1
  95. data/lib/clacky/default_agents/coding/profile.yml +0 -5
  96. data/lib/clacky/default_agents/coding/webui/.gitkeep +0 -0
  97. data/lib/clacky/default_agents/general/profile.yml +0 -2
  98. data/lib/clacky/default_extensions/meeting/meta.yml +0 -3
  99. data/lib/clacky/web/features/creator/store.js +0 -81
  100. data/lib/clacky/web/features/creator/view.js +0 -380
  101. /data/lib/clacky/{default_agents → default_extensions/coding/agents}/coding/system_prompt.md +0 -0
  102. /data/lib/clacky/{default_agents → default_extensions/general/agents}/general/system_prompt.md +0 -0
  103. /data/lib/clacky/{default_agents/base_prompt.md → prompts/base.md} +0 -0
@@ -0,0 +1,186 @@
1
+ // ── NewSession · store — agent list + selection + advanced options ────────
2
+ //
3
+ // Owns the state for the "new session" landing page: the list of agents
4
+ // fetched from /api/agents, the current selection (persisted in localStorage),
5
+ // and the advanced-options form values (name / model / working dir / init).
6
+ // It never renders — NewSessionView owns the DOM.
7
+ //
8
+ // Also handles session creation: POST /api/sessions with the current selection
9
+ // and returns the created session summary. Sending the first message is the
10
+ // caller's job (via Sessions.setPendingMessage + Sessions.select).
11
+ //
12
+ // Depends on: Clacky.ext (for cross-module event emit).
13
+ // ───────────────────────────────────────────────────────────────────────────
14
+
15
+ const NewSessionStore = (() => {
16
+ const LS_AGENT_KEY = "openclacky.newSession.lastAgent";
17
+
18
+ const _state = {
19
+ agents: [],
20
+ selectedAgentId: null,
21
+ advanced: {
22
+ name: "",
23
+ modelId: "",
24
+ workingDir: "",
25
+ initProject: false,
26
+ },
27
+ advancedOpen: false,
28
+ creating: false,
29
+ };
30
+
31
+ const _listeners = {};
32
+
33
+ function _on(event, handler) {
34
+ (_listeners[event] ||= []).push(handler);
35
+ return () => {
36
+ const list = _listeners[event];
37
+ const i = list ? list.indexOf(handler) : -1;
38
+ if (i >= 0) list.splice(i, 1);
39
+ };
40
+ }
41
+
42
+ function _emit(event, payload) {
43
+ (_listeners[event] || []).forEach((h) => h(payload));
44
+ if (window.Clacky && Clacky.ext) Clacky.ext.emit(event, payload);
45
+ }
46
+
47
+ async function loadAgents() {
48
+ try {
49
+ const res = await fetch("/api/agents");
50
+ if (!res.ok) return;
51
+ const data = await res.json();
52
+ _state.agents = (data && data.agents) || [];
53
+
54
+ // Restore last-used agent from localStorage; fall back to the first
55
+ // available one (typically "general").
56
+ const remembered = localStorage.getItem(LS_AGENT_KEY);
57
+ const found = _state.agents.find((a) => a.id === remembered);
58
+ _state.selectedAgentId = found
59
+ ? found.id
60
+ : (_state.agents[0] && _state.agents[0].id) || "general";
61
+
62
+ _emit("newSession:agents-loaded", { agents: _state.agents });
63
+ _emit("newSession:selection-changed", { agentId: _state.selectedAgentId });
64
+ } catch (e) {
65
+ console.error("Failed to load agents:", e);
66
+ }
67
+ }
68
+
69
+ function selectAgent(agentId) {
70
+ if (!agentId || agentId === _state.selectedAgentId) return;
71
+ const found = _state.agents.find((a) => a.id === agentId);
72
+ if (!found) return;
73
+ _state.selectedAgentId = agentId;
74
+ localStorage.setItem(LS_AGENT_KEY, agentId);
75
+ _emit("newSession:selection-changed", { agentId });
76
+ }
77
+
78
+ function currentAgent() {
79
+ return _state.agents.find((a) => a.id === _state.selectedAgentId) || null;
80
+ }
81
+
82
+ function toggleAdvanced(open) {
83
+ _state.advancedOpen = open === undefined ? !_state.advancedOpen : !!open;
84
+ _emit("newSession:advanced-toggled", { open: _state.advancedOpen });
85
+ }
86
+
87
+ function updateAdvanced(patch) {
88
+ Object.assign(_state.advanced, patch);
89
+ _emit("newSession:advanced-changed", { advanced: _state.advanced });
90
+ }
91
+
92
+ async function loadModels() {
93
+ try {
94
+ const res = await fetch("/api/config");
95
+ if (!res.ok) return [];
96
+ const data = await res.json();
97
+ return (data && data.models) || [];
98
+ } catch (e) {
99
+ console.error("Failed to load models:", e);
100
+ return [];
101
+ }
102
+ }
103
+
104
+ async function loadDefaultDirectory() {
105
+ try {
106
+ const res = await fetch("/api/dirs");
107
+ if (!res.ok) return "";
108
+ const data = await res.json();
109
+ const home = data && data.home;
110
+ if (!home) return "";
111
+ return home.replace(/\/+$/, "") + "/clacky_workspace";
112
+ } catch (e) {
113
+ return "";
114
+ }
115
+ }
116
+
117
+ function _autoName(existingSessions) {
118
+ const maxN = (existingSessions || []).reduce((max, s) => {
119
+ const m = s.name && s.name.match(/^Session (\d+)$/);
120
+ return m ? Math.max(max, parseInt(m[1], 10)) : max;
121
+ }, 0);
122
+ return "Session " + (maxN + 1);
123
+ }
124
+
125
+ async function createSession({ existingSessions } = {}) {
126
+ if (_state.creating) return null;
127
+ _state.creating = true;
128
+ _emit("newSession:creating", { creating: true });
129
+
130
+ try {
131
+ const agentId = _state.selectedAgentId || "general";
132
+ const adv = _state.advanced;
133
+ const name = adv.name.trim() || _autoName(existingSessions);
134
+
135
+ const payload = { name, agent_profile: agentId, source: "manual" };
136
+ if (adv.workingDir.trim()) payload.working_dir = adv.workingDir.trim();
137
+ if (adv.modelId) payload.model_id = adv.modelId;
138
+
139
+ const res = await fetch("/api/sessions", {
140
+ method: "POST",
141
+ headers: { "Content-Type": "application/json" },
142
+ body: JSON.stringify(payload),
143
+ });
144
+ const data = await res.json();
145
+
146
+ if (!res.ok) {
147
+ const msg = data.error || "unknown error";
148
+ const friendly = res.status === 409
149
+ ? I18n.t("sessions.dirNotEmpty")
150
+ : I18n.t("sessions.createError") + msg;
151
+ alert(friendly);
152
+ return null;
153
+ }
154
+
155
+ return data.session || null;
156
+ } catch (e) {
157
+ alert(I18n.t("sessions.createError") + e.message);
158
+ return null;
159
+ } finally {
160
+ _state.creating = false;
161
+ _emit("newSession:creating", { creating: false });
162
+ }
163
+ }
164
+
165
+ function reset() {
166
+ _state.advanced.name = "";
167
+ _state.advanced.initProject = false;
168
+ _emit("newSession:reset", {});
169
+ }
170
+
171
+ return {
172
+ get state() { return _state; },
173
+ on: _on,
174
+ loadAgents,
175
+ selectAgent,
176
+ currentAgent,
177
+ toggleAdvanced,
178
+ updateAdvanced,
179
+ loadModels,
180
+ loadDefaultDirectory,
181
+ createSession,
182
+ reset,
183
+ };
184
+ })();
185
+
186
+ window.NewSessionStore = NewSessionStore;
@@ -0,0 +1,319 @@
1
+ // ── NewSession · view — landing page for #new ─────────────────────────────
2
+ //
3
+ // Renders the "start a new session" page: agent card grid, first-message
4
+ // composer, and the collapsible advanced-options section (name / model /
5
+ // working dir / init-project). Reads state from NewSessionStore and delegates
6
+ // I/O to it. On send, creates a session then hands off to Sessions.select()
7
+ // with a pending first message so the existing chat pipeline handles delivery.
8
+ //
9
+ // Depends on: NewSessionStore, Sessions, I18n, Router.
10
+ // ───────────────────────────────────────────────────────────────────────────
11
+
12
+ const NewSessionView = (() => {
13
+ const $ = (id) => document.getElementById(id);
14
+
15
+ let _initialized = false;
16
+ let _modelsLoaded = false;
17
+
18
+ function _isZh() {
19
+ return I18n.lang && I18n.lang().startsWith("zh");
20
+ }
21
+
22
+ function _agentLabel(a) {
23
+ if (_isZh() && a.title_zh) return a.title_zh;
24
+ return a.title || a.id;
25
+ }
26
+
27
+ function _agentDesc(a) {
28
+ if (_isZh() && a.description_zh) return a.description_zh;
29
+ return a.description || "";
30
+ }
31
+
32
+ const LS_SEEN_AGENTS_KEY = "openclacky.newSession.seenAgents";
33
+
34
+ function _seenAgents() {
35
+ try {
36
+ const raw = localStorage.getItem(LS_SEEN_AGENTS_KEY);
37
+ return raw ? JSON.parse(raw) : [];
38
+ } catch (e) {
39
+ return [];
40
+ }
41
+ }
42
+
43
+ function _markAgentSeen(id) {
44
+ if (!id) return;
45
+ const seen = _seenAgents();
46
+ if (seen.includes(id)) return;
47
+ seen.push(id);
48
+ try { localStorage.setItem(LS_SEEN_AGENTS_KEY, JSON.stringify(seen)); } catch (e) { /* ignore */ }
49
+ }
50
+
51
+ // Builtin agents (general/coding/ext-studio) ship with the app and are not
52
+ // "extensions" from the user's point of view — no EXT badge, never NEW.
53
+ function _isBuiltin(a) {
54
+ return a.layer === "builtin";
55
+ }
56
+
57
+ function _isNewAgent(a) {
58
+ if (_isBuiltin(a) || a.source === "user") return false;
59
+ return !_seenAgents().includes(a.id);
60
+ }
61
+
62
+ function _renderAgents() {
63
+ const container = $("new-session-agents");
64
+ if (!container) return;
65
+ const agents = NewSessionStore.state.agents;
66
+ const selected = NewSessionStore.state.selectedAgentId;
67
+
68
+ container.innerHTML = "";
69
+ agents.forEach((a) => {
70
+ const card = document.createElement("button");
71
+ card.type = "button";
72
+ card.className = "agent-card" + (a.id === selected ? " agent-card--selected" : "");
73
+ card.dataset.agentId = a.id;
74
+
75
+ const title = document.createElement("div");
76
+ title.className = "agent-card-title";
77
+ title.textContent = _agentLabel(a);
78
+ if (_isNewAgent(a)) {
79
+ const isNew = document.createElement("span");
80
+ isNew.className = "agent-card-new";
81
+ isNew.textContent = "NEW";
82
+ title.appendChild(isNew);
83
+ }
84
+ card.appendChild(title);
85
+
86
+ const descText = _agentDesc(a);
87
+ if (descText) {
88
+ const desc = document.createElement("div");
89
+ desc.className = "agent-card-desc";
90
+ desc.textContent = descText;
91
+ card.appendChild(desc);
92
+ }
93
+
94
+ const author = (a.author || "").trim();
95
+ if (author) {
96
+ const by = document.createElement("div");
97
+ by.className = "agent-card-author";
98
+ by.textContent = _isZh() ? `作者 ${author}` : `by ${author}`;
99
+ card.appendChild(by);
100
+ }
101
+
102
+ if (!_isBuiltin(a) && a.source === "extension") {
103
+ const badge = document.createElement("span");
104
+ badge.className = "agent-card-badge";
105
+ badge.textContent = "EXT";
106
+ card.appendChild(badge);
107
+ } else if (a.source === "user") {
108
+ const badge = document.createElement("span");
109
+ badge.className = "agent-card-badge agent-card-badge--custom";
110
+ badge.textContent = "custom";
111
+ card.appendChild(badge);
112
+ }
113
+
114
+ card.addEventListener("click", () => {
115
+ _markAgentSeen(a.id);
116
+ NewSessionStore.selectAgent(a.id);
117
+ _renderAgents();
118
+ });
119
+ container.appendChild(card);
120
+ });
121
+
122
+ _updatePlaceholder();
123
+ }
124
+
125
+ function _updatePlaceholder() {
126
+ const input = $("new-session-input");
127
+ if (!input) return;
128
+ const agent = NewSessionStore.currentAgent();
129
+ const label = agent ? _agentLabel(agent) : "";
130
+ const tpl = I18n.t("sessions.new.placeholder");
131
+ input.placeholder = label
132
+ ? tpl.replace("{{agent}}", label)
133
+ : tpl.replace("{{agent}}", "");
134
+ }
135
+
136
+ async function _populateModels() {
137
+ if (_modelsLoaded) return;
138
+ const select = $("new-session-model");
139
+ if (!select) return;
140
+ const models = await NewSessionStore.loadModels();
141
+
142
+ select.innerHTML = "";
143
+ if (models.length === 0) {
144
+ const opt = document.createElement("option");
145
+ opt.value = "";
146
+ opt.textContent = "No models configured";
147
+ select.appendChild(opt);
148
+ _modelsLoaded = true;
149
+ return;
150
+ }
151
+
152
+ models.forEach((m) => {
153
+ const opt = document.createElement("option");
154
+ opt.value = m.id || "";
155
+ const typeBadge = m.type === "default" ? "[default] " : "";
156
+ opt.textContent = `${typeBadge}${m.model} (${m.api_key_masked})`;
157
+ if (m.type === "default") {
158
+ opt.selected = true;
159
+ NewSessionStore.updateAdvanced({ modelId: opt.value });
160
+ }
161
+ select.appendChild(opt);
162
+ });
163
+ _modelsLoaded = true;
164
+ }
165
+
166
+ async function _prefillDefaultDir() {
167
+ const dirInput = $("new-session-directory");
168
+ if (!dirInput || dirInput.value) return;
169
+ const home = await NewSessionStore.loadDefaultDirectory();
170
+ if (home && !dirInput.value) {
171
+ dirInput.value = home;
172
+ NewSessionStore.updateAdvanced({ workingDir: home });
173
+ }
174
+ }
175
+
176
+ function _updateInitProjectVisibility() {
177
+ const field = $("new-session-init-project-field");
178
+ if (!field) return;
179
+ const agent = NewSessionStore.currentAgent();
180
+ field.style.display = agent && agent.id === "coding" ? "block" : "none";
181
+ }
182
+
183
+ function _updateSendButton() {
184
+ const btn = $("new-session-send");
185
+ const input = $("new-session-input");
186
+ if (!btn || !input) return;
187
+ const hasText = input.value.trim().length > 0;
188
+ btn.disabled = !hasText || NewSessionStore.state.creating;
189
+ }
190
+
191
+ async function _submit() {
192
+ const input = $("new-session-input");
193
+ if (!input) return;
194
+ const content = input.value.trim();
195
+ if (!content) return;
196
+
197
+ const initCheckbox = $("new-session-init-project");
198
+ const initProject = initCheckbox && initCheckbox.checked;
199
+
200
+ const btn = $("new-session-send");
201
+ if (btn) btn.disabled = true;
202
+
203
+ const session = await NewSessionStore.createSession({
204
+ existingSessions: (Sessions && Sessions.all) || [],
205
+ });
206
+ if (!session) {
207
+ _updateSendButton();
208
+ return;
209
+ }
210
+
211
+ Sessions.add(session);
212
+ Sessions.setPendingMessage(session.id, content);
213
+ input.value = "";
214
+ NewSessionStore.reset();
215
+
216
+ // Hand off to the normal chat pipeline: this switches the panel, wires
217
+ // up WS subscription, and the pending message is sent once subscribed
218
+ // (see ws-dispatcher.js "subscribed" branch).
219
+ Sessions.select(session.id);
220
+ }
221
+
222
+ function _bindOnce() {
223
+ if (_initialized) return;
224
+ _initialized = true;
225
+
226
+ NewSessionStore.on("newSession:agents-loaded", _renderAgents);
227
+ NewSessionStore.on("newSession:selection-changed", () => {
228
+ _renderAgents();
229
+ _updateInitProjectVisibility();
230
+ });
231
+ NewSessionStore.on("newSession:creating", _updateSendButton);
232
+
233
+ const input = $("new-session-input");
234
+ if (input) {
235
+ input.addEventListener("input", _updateSendButton);
236
+ input.addEventListener("keydown", (e) => {
237
+ if (e.key === "Enter" && !e.shiftKey && !e.isComposing) {
238
+ e.preventDefault();
239
+ _submit();
240
+ }
241
+ });
242
+ }
243
+
244
+ const sendBtn = $("new-session-send");
245
+ if (sendBtn) sendBtn.addEventListener("click", _submit);
246
+
247
+ const toggle = $("new-session-toggle-advanced");
248
+ const advanced = $("new-session-advanced");
249
+ if (toggle && advanced) {
250
+ toggle.addEventListener("click", async () => {
251
+ const open = advanced.hidden;
252
+ advanced.hidden = !open;
253
+ toggle.classList.toggle("is-open", open);
254
+ if (open) {
255
+ await _populateModels();
256
+ await _prefillDefaultDir();
257
+ }
258
+ NewSessionStore.toggleAdvanced(open);
259
+ });
260
+ }
261
+
262
+ const nameInput = $("new-session-name");
263
+ if (nameInput) {
264
+ nameInput.addEventListener("input", (e) =>
265
+ NewSessionStore.updateAdvanced({ name: e.target.value })
266
+ );
267
+ }
268
+
269
+ const modelSelect = $("new-session-model");
270
+ if (modelSelect) {
271
+ modelSelect.addEventListener("change", (e) =>
272
+ NewSessionStore.updateAdvanced({ modelId: e.target.value })
273
+ );
274
+ }
275
+
276
+ const dirInput = $("new-session-directory");
277
+ if (dirInput) {
278
+ dirInput.addEventListener("input", (e) =>
279
+ NewSessionStore.updateAdvanced({ workingDir: e.target.value })
280
+ );
281
+ }
282
+
283
+ const browseBtn = $("new-session-browse-btn");
284
+ if (browseBtn && dirInput) {
285
+ browseBtn.addEventListener("click", async () => {
286
+ const start = dirInput.value.trim();
287
+ const picked = await window.openDirectoryPicker(start, null);
288
+ if (picked) {
289
+ dirInput.value = picked;
290
+ NewSessionStore.updateAdvanced({ workingDir: picked });
291
+ }
292
+ });
293
+ }
294
+
295
+ const initCheckbox = $("new-session-init-project");
296
+ if (initCheckbox) {
297
+ initCheckbox.addEventListener("change", (e) =>
298
+ NewSessionStore.updateAdvanced({ initProject: e.target.checked })
299
+ );
300
+ }
301
+ }
302
+
303
+ async function onPanelShow() {
304
+ _bindOnce();
305
+ if (NewSessionStore.state.agents.length === 0) {
306
+ await NewSessionStore.loadAgents();
307
+ } else {
308
+ _renderAgents();
309
+ }
310
+ _updateInitProjectVisibility();
311
+ _updateSendButton();
312
+ const input = $("new-session-input");
313
+ if (input) input.focus();
314
+ }
315
+
316
+ return { onPanelShow };
317
+ })();
318
+
319
+ window.NewSessionView = NewSessionView;
@@ -13,6 +13,11 @@
13
13
  // ───────────────────────────────────────────────────────────────────────────
14
14
 
15
15
  const ShareView = (() => {
16
+ const THEME_DOT = {
17
+ geek: "#38bdf8",
18
+ light: "#6ee7b7",
19
+ warm: "#ea580c"
20
+ };
16
21
  const THEME_KEY = "clacky-share-theme";
17
22
  const DRAFT_KEY = "clacky-share-draft";
18
23
 
@@ -158,32 +163,49 @@ const ShareView = (() => {
158
163
 
159
164
  _paintBackground(ctx, W, H, t, t.bg);
160
165
 
166
+ // ── Brand name ──
161
167
  ctx.fillStyle = t.title;
162
- ctx.textAlign = "center";
163
- ctx.font = "700 60px -apple-system, 'PingFang SC', 'Microsoft YaHei', sans-serif";
164
- ctx.fillText(_brandName(), W / 2, 150);
168
+ ctx.textAlign = "left";
169
+ ctx.font = "800 72px -apple-system, 'PingFang SC', 'Microsoft YaHei', sans-serif";
170
+ ctx.fillText(_brandName(), 64, 140);
171
+
172
+ // ── Divider line ──
173
+ ctx.strokeStyle = t.hero;
174
+ ctx.lineWidth = 4;
175
+ ctx.lineCap = "round";
176
+ ctx.beginPath();
177
+ ctx.moveTo(64, 172);
178
+ ctx.lineTo(160, 172);
179
+ ctx.stroke();
165
180
 
181
+ // ── Copy text ──
166
182
  const text = (copy || "").trim() || I18n.t("share.poster.tagline", { brand: _brandName() });
167
183
  ctx.fillStyle = t.tagline;
168
- _drawAutoText(ctx, text, W / 2, 250, W - 120, 480 - 250);
184
+ ctx.textAlign = "left";
185
+ _drawAutoTextLeft(ctx, text, 64, 210, W - 128, 420);
169
186
 
170
187
  const url = _shareUrl();
171
188
  if (url) {
172
- const qrSize = 300;
173
- const qrX = (W - qrSize) / 2;
174
- const qrY = 560;
189
+ // ── QR code — bottom right ──
190
+ const qrSize = 180;
191
+ const qrX = W - qrSize - 56;
192
+ const qrY = H - qrSize - 100;
175
193
  ctx.fillStyle = "#ffffff";
176
- _roundRect(ctx, qrX - 24, qrY - 24, qrSize + 48, qrSize + 48, 22);
194
+ _roundRect(ctx, qrX - 14, qrY - 14, qrSize + 28, qrSize + 28, 16);
177
195
  ctx.fill();
178
196
  _drawQrToCanvas(ctx, url, qrX, qrY, qrSize);
179
197
 
198
+ // ── Scan label under QR ──
180
199
  ctx.fillStyle = t.scan;
181
- ctx.font = "400 28px -apple-system, 'PingFang SC', 'Microsoft YaHei', sans-serif";
182
- ctx.fillText(I18n.t("share.poster.scan"), W / 2, qrY + qrSize + 80);
200
+ ctx.textAlign = "center";
201
+ ctx.font = "400 22px -apple-system, 'PingFang SC', 'Microsoft YaHei', sans-serif";
202
+ ctx.fillText(I18n.t("share.poster.scan"), qrX + qrSize / 2, qrY + qrSize + 44);
183
203
 
204
+ // ── Domain — bottom left ──
184
205
  ctx.fillStyle = t.scan;
185
- ctx.font = "400 24px -apple-system, sans-serif";
186
- ctx.fillText(url.replace(/^https?:\/\//, "").replace(/\/$/, ""), W / 2, H - 60);
206
+ ctx.textAlign = "left";
207
+ ctx.font = "300 24px -apple-system, sans-serif";
208
+ ctx.fillText(url.replace(/^https?:\/\//, "").replace(/\/$/, ""), 64, H - 56);
187
209
  }
188
210
 
189
211
  return canvas.toDataURL("image/png");
@@ -331,6 +353,27 @@ const ShareView = (() => {
331
353
  }
332
354
  }
333
355
 
356
+ function _drawAutoTextLeft(ctx, text, x, startY, maxWidth, maxHeight) {
357
+ const sizes = [52, 46, 40, 36, 32, 28, 24];
358
+ let chosen = null;
359
+ for (const size of sizes) {
360
+ ctx.font = "600 " + size + "px -apple-system, 'PingFang SC', 'Microsoft YaHei', sans-serif";
361
+ const lh = Math.round(size * 1.55);
362
+ const lines = _wrapLines(ctx, text, maxWidth);
363
+ if (lines.length * lh <= maxHeight || size === sizes[sizes.length - 1]) {
364
+ chosen = { size, lh, lines };
365
+ break;
366
+ }
367
+ }
368
+ ctx.font = "600 " + chosen.size + "px -apple-system, 'PingFang SC', 'Microsoft YaHei', sans-serif";
369
+ ctx.textAlign = "left";
370
+ let y = startY + chosen.lh * 0.8;
371
+ for (const line of chosen.lines) {
372
+ ctx.fillText(line, x, y);
373
+ y += chosen.lh;
374
+ }
375
+ }
376
+
334
377
  function _wrapLines(ctx, text, maxWidth) {
335
378
  const out = [];
336
379
  for (const para of String(text).split("\n")) {
@@ -469,9 +512,10 @@ const ShareView = (() => {
469
512
  const themeChips = THEME_ORDER.map((id) => {
470
513
  const th = THEMES[id];
471
514
  const on = id === _themeId() ? " is-active" : "";
515
+ const dot = '<span class="share-theme-dot" style="background:' + THEME_DOT[id] + '"></span>';
472
516
  return '<button type="button" class="share-theme-chip' + on + '" data-theme="' + id + '"' +
473
517
  ' style="background:' + th.swatch + '" title="' + _esc(I18n.t(th.labelKey)) + '">' +
474
- '<span class="share-theme-name">' + _esc(I18n.t(th.labelKey)) + '</span></button>';
518
+ dot + '<span class="share-theme-name">' + _esc(I18n.t(th.labelKey)) + '</span></button>';
475
519
  }).join("");
476
520
 
477
521
  const platformTabs = PLATFORMS.map((p) => {
@@ -486,7 +530,9 @@ const ShareView = (() => {
486
530
  '<h3 class="share-title">' + _esc(I18n.t(titleKey, { brand: _brandName() })) + '</h3>' +
487
531
  '<p class="share-subtitle">' + _esc(I18n.t(subtitleKey)) + '</p>' +
488
532
  '<div class="share-body">' +
489
- '<div class="share-poster-wrap"><img class="share-poster-img" alt="poster"/></div>' +
533
+ '<div class="share-poster-wrap">' +
534
+ '<img class="share-poster-img" alt="poster"/>' +
535
+ '</div>' +
490
536
  '<div class="share-controls">' +
491
537
  '<div class="share-theme-row">' +
492
538
  '<span class="share-row-label">' + _esc(I18n.t("share.theme.label")) + '</span>' +
@@ -517,6 +563,9 @@ const ShareView = (() => {
517
563
  const img = o.querySelector(".share-poster-img");
518
564
  const textarea = o.querySelector(".share-text");
519
565
 
566
+ img.style.cursor = "pointer";
567
+ img.addEventListener("click", () => _downloadPoster((textarea.value || "").trim()));
568
+
520
569
  const renderPoster = () => {
521
570
  const copy = (textarea.value || "").trim();
522
571
  try { img.src = _scorecard() ? _buildScorecardPoster(copy) : _buildPoster(copy); }
@@ -30,7 +30,7 @@ const SkillsStore = (() => {
30
30
  let _brandActivated = false; // whether a license is currently active
31
31
  let _freeMode = false; // brand-skills tab is showing free-mode skills
32
32
  let _paidSkillsCount = 0; // premium (encrypted) skills locked behind activation
33
- let _showSystemSkills = false; // whether system (source=default) skills are shown
33
+ let _showSystemSkills = localStorage.getItem("skill-ac-show-system") === "true";
34
34
 
35
35
  // ── Internal event bus ──────────────────────────────────────────────────
36
36
  // Always live (unlike Clacky.ext, which is silenced under ?pure=true). The
@@ -173,6 +173,7 @@ const SkillsStore = (() => {
173
173
  /** Toggle visibility of system skills; emit so the view re-renders. */
174
174
  setShowSystemSkills(show) {
175
175
  _showSystemSkills = !!show;
176
+ localStorage.setItem("skill-ac-show-system", _showSystemSkills ? "true" : "false");
176
177
  _emit("skills:changed", { skills: _skills });
177
178
  },
178
179
 
@@ -314,3 +315,4 @@ const SkillsStore = (() => {
314
315
 
315
316
  // Expose the facade under its historical global name.
316
317
  const Skills = SkillsStore;
318
+ Clacky.Skills = Skills;
@@ -265,7 +265,7 @@ const SkillsView = (() => {
265
265
  btn.setAttribute("data-i18n", "skills.brand.activateBtn");
266
266
  btn.addEventListener("click", () => {
267
267
  if (typeof Brand !== "undefined" && Brand.goToLicenseInput) Brand.goToLicenseInput();
268
- else Router.navigate("settings");
268
+ else window.Clacky.Router.navigate("settings");
269
269
  });
270
270
 
271
271
  hint.appendChild(msgEl);