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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +30 -0
- data/lib/clacky/agent.rb +8 -1
- data/lib/clacky/agent_profile.rb +121 -53
- data/lib/clacky/brand_config.rb +309 -6
- data/lib/clacky/cli.rb +4 -57
- data/lib/clacky/default_extensions/coding/ext.yml +18 -0
- data/lib/clacky/default_extensions/ext-studio/agents/ext-developer/system_prompt.md +95 -0
- data/lib/clacky/default_extensions/ext-studio/api/handler.rb +254 -0
- data/lib/clacky/default_extensions/ext-studio/ext.yml +32 -0
- data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +1269 -0
- data/lib/clacky/default_extensions/ext-studio/skills/ext-debug/SKILL.md +71 -0
- data/lib/clacky/default_extensions/ext-studio/skills/ext-publish/SKILL.md +74 -0
- data/lib/clacky/default_extensions/ext-studio/skills/ext-scaffold/SKILL.md +65 -0
- data/lib/clacky/default_extensions/general/ext.yml +18 -0
- data/lib/clacky/default_extensions/git/ext.yml +12 -0
- data/lib/clacky/{default_agents/_panels/git/panel.js → default_extensions/git/panels/git/view.js} +1 -2
- data/lib/clacky/default_extensions/meeting/{handler.rb → api/handler.rb} +78 -26
- data/lib/clacky/default_extensions/meeting/ext.yml +15 -0
- data/lib/clacky/default_extensions/meeting/{meeting.js → panels/meeting/view.js} +266 -89
- data/lib/clacky/default_extensions/meeting/skills/meeting-summarizer/SKILL.md +2 -1
- data/lib/clacky/default_extensions/time_machine/ext.yml +12 -0
- data/lib/clacky/{default_agents/_panels/time_machine/panel.js → default_extensions/time_machine/panels/time_machine/view.js} +58 -47
- data/lib/clacky/default_skills/channel-manager/discord_setup.rb +1 -1
- data/lib/clacky/default_skills/extend-openclacky/SKILL.md +83 -18
- data/lib/clacky/{api_extension.rb → extension/api_extension.rb} +27 -11
- data/lib/clacky/extension/api_loader.rb +136 -0
- data/lib/clacky/extension/cli_commands.rb +226 -0
- data/lib/clacky/{server/api_extension_dispatcher.rb → extension/dispatcher.rb} +22 -9
- data/lib/clacky/extension/hook_loader.rb +77 -0
- data/lib/clacky/extension/loader.rb +483 -0
- data/lib/clacky/extension/packager.rb +226 -0
- data/lib/clacky/{patch_loader.rb → extension/patch_loader.rb} +45 -0
- data/lib/clacky/extension/scaffold/template_renderer.rb +64 -0
- data/lib/clacky/extension/scaffold/templates/full/README.md.erb +48 -0
- data/lib/clacky/extension/scaffold/templates/full/agents/designer.md +5 -0
- data/lib/clacky/extension/scaffold/templates/full/api/handler.rb.erb +16 -0
- data/lib/clacky/extension/scaffold/templates/full/channels/noop.rb.erb +40 -0
- data/lib/clacky/extension/scaffold/templates/full/ext.yml.erb +41 -0
- data/lib/clacky/extension/scaffold/templates/full/hooks/audit.rb +9 -0
- data/lib/clacky/extension/scaffold/templates/full/panels/dashboard/view.js.erb +194 -0
- data/lib/clacky/extension/scaffold/templates/full/patches/audit.rb +15 -0
- data/lib/clacky/extension/scaffold/templates/full/skills/__slug__-skill/SKILL.md.erb +8 -0
- data/lib/clacky/extension/scaffold/templates/hello/api/handler.rb.erb +9 -0
- data/lib/clacky/extension/scaffold/templates/hello/ext.yml.erb +12 -0
- data/lib/clacky/extension/scaffold/templates/hello/panels/hello/view.js.erb +31 -0
- data/lib/clacky/extension/scaffold.rb +55 -0
- data/lib/clacky/extension/verifier.rb +196 -0
- data/lib/clacky/identity.rb +70 -0
- data/lib/clacky/locales/en.rb +19 -1
- data/lib/clacky/locales/zh.rb +19 -1
- data/lib/clacky/platform_http_client.rb +18 -15
- data/lib/clacky/server/channel/extension_adapter_loader.rb +32 -0
- data/lib/clacky/server/channel.rb +6 -0
- data/lib/clacky/server/http_server.rb +313 -144
- data/lib/clacky/server/session_registry.rb +9 -2
- data/lib/clacky/skill_loader.rb +28 -1
- data/lib/clacky/ui2/ui_controller.rb +9 -6
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +748 -573
- data/lib/clacky/web/app.js +63 -15
- data/lib/clacky/web/auth.js +2 -0
- data/lib/clacky/web/components/notify.js +2 -0
- data/lib/clacky/web/components/sidebar.js +1 -5
- data/lib/clacky/web/core/ext.js +316 -79
- data/lib/clacky/web/features/backup/store.js +1 -0
- data/lib/clacky/web/features/backup/view.js +0 -1
- data/lib/clacky/web/features/brand/view.js +13 -5
- data/lib/clacky/web/features/extensions/store.js +184 -0
- data/lib/clacky/web/features/extensions/view.js +389 -0
- data/lib/clacky/web/features/new-session/store.js +186 -0
- data/lib/clacky/web/features/new-session/view.js +319 -0
- data/lib/clacky/web/features/share/view.js +63 -14
- data/lib/clacky/web/features/skills/store.js +3 -1
- data/lib/clacky/web/features/skills/view.js +1 -1
- data/lib/clacky/web/features/tasks/view.js +12 -0
- data/lib/clacky/web/features/trash/store.js +24 -6
- data/lib/clacky/web/features/trash/view.js +46 -9
- data/lib/clacky/web/features/workspace/store.js +2 -0
- data/lib/clacky/web/features/workspace/view.js +0 -1
- data/lib/clacky/web/i18n.js +118 -106
- data/lib/clacky/web/index.html +138 -188
- data/lib/clacky/web/sessions.js +64 -269
- data/lib/clacky/web/settings.js +1 -1
- data/lib/clacky/web/skills.js +2 -0
- data/lib/clacky/web/theme.js +2 -0
- data/lib/clacky/web/ws-dispatcher.js +1 -0
- data/lib/clacky/web/ws.js +2 -0
- data/lib/clacky.rb +21 -10
- metadata +55 -24
- data/benchmark/runner.rb +0 -441
- data/lib/clacky/api_extension_loader.rb +0 -168
- data/lib/clacky/default_agents/SOUL.md +0 -3
- data/lib/clacky/default_agents/USER.md +0 -1
- data/lib/clacky/default_agents/coding/profile.yml +0 -5
- data/lib/clacky/default_agents/coding/webui/.gitkeep +0 -0
- data/lib/clacky/default_agents/general/profile.yml +0 -2
- data/lib/clacky/default_extensions/meeting/meta.yml +0 -3
- data/lib/clacky/web/features/creator/store.js +0 -81
- data/lib/clacky/web/features/creator/view.js +0 -380
- /data/lib/clacky/{default_agents → default_extensions/coding/agents}/coding/system_prompt.md +0 -0
- /data/lib/clacky/{default_agents → default_extensions/general/agents}/general/system_prompt.md +0 -0
- /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 = "
|
|
163
|
-
ctx.font = "
|
|
164
|
-
ctx.fillText(_brandName(),
|
|
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
|
-
|
|
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
|
-
|
|
173
|
-
const
|
|
174
|
-
const
|
|
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 -
|
|
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.
|
|
182
|
-
ctx.
|
|
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.
|
|
186
|
-
ctx.
|
|
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"
|
|
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 =
|
|
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);
|