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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +44 -0
- data/Dockerfile +3 -0
- data/README.md +1 -1
- data/README_JA.md +237 -0
- data/lib/clacky/agent/session_serializer.rb +65 -11
- data/lib/clacky/agent/time_machine.rb +247 -26
- data/lib/clacky/agent.rb +12 -1
- data/lib/clacky/agent_config.rb +14 -2
- data/lib/clacky/brand_config.rb +1 -1
- data/lib/clacky/default_agents/_panels/git/panel.js +201 -0
- data/lib/clacky/default_agents/_panels/time_machine/panel.js +640 -0
- data/lib/clacky/default_agents/coding/profile.yml +3 -0
- data/lib/clacky/default_agents/coding/webui/.gitkeep +0 -0
- data/lib/clacky/default_skills/cron-task-creator/SKILL.md +1 -1
- data/lib/clacky/default_skills/extend-openclacky/SKILL.md +6 -4
- data/lib/clacky/default_skills/media-gen/SKILL.md +30 -6
- data/lib/clacky/media/openai_compat.rb +64 -1
- data/lib/clacky/media/output_dir.rb +43 -0
- data/lib/clacky/message_history.rb +9 -0
- data/lib/clacky/server/channel/channel_manager.rb +26 -0
- data/lib/clacky/server/git_panel.rb +115 -0
- data/lib/clacky/server/http_server.rb +521 -13
- data/lib/clacky/server/server_master.rb +6 -4
- data/lib/clacky/utils/environment_detector.rb +16 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +512 -60
- data/lib/clacky/web/app.js +30 -7
- data/lib/clacky/web/components/code-editor.js +197 -0
- data/lib/clacky/web/{notify.js → components/notify.js} +1 -1
- data/lib/clacky/web/core/aside.js +112 -0
- data/lib/clacky/web/core/ext.js +387 -0
- data/lib/clacky/web/features/backup/store.js +92 -0
- data/lib/clacky/web/features/backup/view.js +94 -0
- data/lib/clacky/web/features/billing/store.js +163 -0
- data/lib/clacky/web/{billing.js → features/billing/view.js} +134 -242
- data/lib/clacky/web/features/brand/store.js +110 -0
- data/lib/clacky/web/{brand.js → features/brand/view.js} +49 -199
- data/lib/clacky/web/features/channels/store.js +103 -0
- data/lib/clacky/web/{channels.js → features/channels/view.js} +50 -127
- data/lib/clacky/web/features/creator/store.js +81 -0
- data/lib/clacky/web/{creator.js → features/creator/view.js} +53 -102
- data/lib/clacky/web/features/mcp/store.js +158 -0
- data/lib/clacky/web/{mcp.js → features/mcp/view.js} +57 -134
- data/lib/clacky/web/features/model-tester/store.js +77 -0
- data/lib/clacky/web/features/model-tester/view.js +7 -0
- data/lib/clacky/web/features/profile/store.js +170 -0
- data/lib/clacky/web/{profile.js → features/profile/view.js} +94 -144
- data/lib/clacky/web/features/share/store.js +145 -0
- data/lib/clacky/web/{share.js → features/share/view.js} +66 -202
- data/lib/clacky/web/features/skills/store.js +303 -0
- data/lib/clacky/web/features/skills/view.js +550 -0
- data/lib/clacky/web/features/tasks/store.js +135 -0
- data/lib/clacky/web/features/tasks/view.js +241 -0
- data/lib/clacky/web/features/trash/store.js +242 -0
- data/lib/clacky/web/{trash.js → features/trash/view.js} +102 -293
- data/lib/clacky/web/features/version/store.js +165 -0
- data/lib/clacky/web/features/version/view.js +323 -0
- data/lib/clacky/web/features/workspace/store.js +99 -0
- data/lib/clacky/web/features/workspace/view.js +305 -0
- data/lib/clacky/web/i18n.js +60 -6
- data/lib/clacky/web/index.html +117 -57
- data/lib/clacky/web/sessions.js +221 -25
- data/lib/clacky/web/settings.js +121 -25
- data/lib/clacky/web/skills.js +3 -821
- data/lib/clacky/web/vendor/codemirror/codemirror.min.js +29 -0
- data/lib/clacky.rb +1 -0
- metadata +45 -20
- data/lib/clacky/web/backup.js +0 -119
- data/lib/clacky/web/model-tester.js +0 -66
- data/lib/clacky/web/tasks.js +0 -365
- data/lib/clacky/web/version.js +0 -449
- data/lib/clacky/web/workspace.js +0 -212
- /data/lib/clacky/web/{notify.mp3 → assets/notify.mp3} +0 -0
- /data/lib/clacky/web/{datepicker.js → components/datepicker.js} +0 -0
- /data/lib/clacky/web/{onboard.js → components/onboard.js} +0 -0
- /data/lib/clacky/web/{sidebar.js → components/sidebar.js} +0 -0
- /data/lib/clacky/web/{marked.min.js → vendor/marked/marked.min.js} +0 -0
data/lib/clacky/web/skills.js
CHANGED
|
@@ -1,824 +1,6 @@
|
|
|
1
|
-
// ──
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// - Single source of truth for skills data
|
|
5
|
-
// - Render the "Skills" entry in the sidebar
|
|
6
|
-
// - Show/render the skills panel with My Skills / Brand Skills tabs
|
|
7
|
-
// - Toggle enable/disable via PATCH /api/skills/:name/toggle
|
|
8
|
-
// - Create new skill by opening a session with /skill-creator
|
|
9
|
-
//
|
|
10
|
-
// Panel switching is delegated to Router — Skills only manages data + rendering.
|
|
11
|
-
//
|
|
12
|
-
// Depends on: WS (ws.js), Sessions (sessions.js), Router (app.js),
|
|
13
|
-
// global $ / escapeHtml helpers
|
|
14
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
15
|
-
|
|
16
|
-
const Skills = (() => {
|
|
17
|
-
// ── Private state ──────────────────────────────────────────────────────
|
|
18
|
-
let _skills = []; // [{ name, description, source, enabled }]
|
|
19
|
-
let _brandSkills = []; // skills from cloud license API
|
|
20
|
-
let _activeTab = "my-skills"; // "my-skills" | "brand-skills"
|
|
21
|
-
let _brandActivated = false; // whether a license is currently active
|
|
22
|
-
let _freeMode = false; // brand-skills tab is showing free-mode skills
|
|
23
|
-
let _paidSkillsCount = 0; // count of premium (encrypted) skills locked behind activation
|
|
24
|
-
let _domWired = false; // whether one-time DOM listeners have been bound
|
|
25
|
-
let _showSystemSkills = false; // whether system (source=default) skills are shown
|
|
26
|
-
|
|
27
|
-
// ── Private helpers ────────────────────────────────────────────────────
|
|
28
|
-
|
|
29
|
-
/** Switch tabs inside the skills panel. */
|
|
30
|
-
function _switchTab(tab) {
|
|
31
|
-
_activeTab = tab;
|
|
32
|
-
document.querySelectorAll(".skills-tab").forEach(btn => {
|
|
33
|
-
btn.classList.toggle("active", btn.dataset.tab === tab);
|
|
34
|
-
});
|
|
35
|
-
$("skills-tab-my").style.display = tab === "my-skills" ? "" : "none";
|
|
36
|
-
$("skills-tab-brand").style.display = tab === "brand-skills" ? "" : "none";
|
|
37
|
-
|
|
38
|
-
// Toggle visibility of tab-specific controls in the tab bar
|
|
39
|
-
const showSystemLabel = $("label-show-system");
|
|
40
|
-
const refreshBtn = $("btn-refresh-brand-skills");
|
|
41
|
-
if (showSystemLabel) showSystemLabel.style.display = tab === "my-skills" ? "" : "none";
|
|
42
|
-
if (refreshBtn) refreshBtn.style.display = tab === "brand-skills" ? "" : "none";
|
|
43
|
-
|
|
44
|
-
// Refresh brand skills every time the tab is opened
|
|
45
|
-
if (tab === "brand-skills") {
|
|
46
|
-
_loadBrandSkills();
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** Fetch brand skills from the server and re-render the tab. */
|
|
51
|
-
async function _loadBrandSkills() {
|
|
52
|
-
const container = $("brand-skills-list");
|
|
53
|
-
if (!container) return;
|
|
54
|
-
container.innerHTML = `<div class="brand-skills-loading">${I18n.t("skills.loading")}</div>`;
|
|
55
|
-
|
|
56
|
-
try {
|
|
57
|
-
const res = await fetch("/api/brand/skills");
|
|
58
|
-
const data = await res.json();
|
|
59
|
-
|
|
60
|
-
if (!res.ok || !data.ok) {
|
|
61
|
-
container.innerHTML = '<div class="brand-skills-error">' + escapeHtml(data.error || I18n.t("skills.brand.loadFailed")) + "</div>";
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
_brandSkills = data.skills || [];
|
|
66
|
-
_freeMode = !!data.free_mode;
|
|
67
|
-
_paidSkillsCount = Number(data.paid_skills_count) || 0;
|
|
68
|
-
|
|
69
|
-
// Soft warning: remote API unavailable but local skills returned.
|
|
70
|
-
// Prefer the server-provided warning_code for proper i18n; fall back to
|
|
71
|
-
// the raw `warning` string (legacy / non-codified messages).
|
|
72
|
-
const warningBanner = $("brand-skills-warning");
|
|
73
|
-
const warningText = data.warning_code
|
|
74
|
-
? I18n.t("skills.brand.warning." + data.warning_code)
|
|
75
|
-
: data.warning;
|
|
76
|
-
if (warningText) {
|
|
77
|
-
if (warningBanner) {
|
|
78
|
-
warningBanner.textContent = warningText;
|
|
79
|
-
// Let i18n re-render pick this up on language switch.
|
|
80
|
-
if (data.warning_code) {
|
|
81
|
-
warningBanner.setAttribute("data-i18n", "skills.brand.warning." + data.warning_code);
|
|
82
|
-
} else {
|
|
83
|
-
warningBanner.removeAttribute("data-i18n");
|
|
84
|
-
}
|
|
85
|
-
warningBanner.style.display = "";
|
|
86
|
-
}
|
|
87
|
-
} else {
|
|
88
|
-
if (warningBanner) {
|
|
89
|
-
warningBanner.style.display = "none";
|
|
90
|
-
warningBanner.removeAttribute("data-i18n");
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
_renderBrandSkills();
|
|
95
|
-
} catch (e) {
|
|
96
|
-
container.innerHTML = '<div class="brand-skills-error">Network error \u2014 please try again.</div>';
|
|
97
|
-
console.error("[Skills] brand skills load failed", e);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/** Render all brand skills into the brand-skills tab. */
|
|
102
|
-
function _renderBrandSkills() {
|
|
103
|
-
const container = $("brand-skills-list");
|
|
104
|
-
if (!container) return;
|
|
105
|
-
container.innerHTML = "";
|
|
106
|
-
|
|
107
|
-
if (_brandSkills.length === 0 && !(_freeMode && _paidSkillsCount > 0)) {
|
|
108
|
-
container.innerHTML = `<div class="brand-skills-empty">${I18n.t("skills.brand.empty")}</div>`;
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
_brandSkills.forEach(skill => {
|
|
113
|
-
const card = _renderBrandSkillCard(skill);
|
|
114
|
-
container.appendChild(card);
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
// Free mode + premium skills exist → show a footer hint inviting the user to activate.
|
|
118
|
-
if (_freeMode && _paidSkillsCount > 0) {
|
|
119
|
-
const hint = document.createElement("div");
|
|
120
|
-
hint.className = "brand-skills-paid-hint";
|
|
121
|
-
|
|
122
|
-
const msgEl = document.createElement("div");
|
|
123
|
-
msgEl.className = "brand-skills-paid-hint-msg";
|
|
124
|
-
msgEl.textContent = I18n.t("skills.brand.paidHint", { n: _paidSkillsCount });
|
|
125
|
-
msgEl.setAttribute("data-i18n", "skills.brand.paidHint");
|
|
126
|
-
msgEl.setAttribute("data-i18n-vars", `n=${_paidSkillsCount}`);
|
|
127
|
-
|
|
128
|
-
const btn = document.createElement("button");
|
|
129
|
-
btn.className = "brand-skills-activate-btn";
|
|
130
|
-
btn.textContent = I18n.t("skills.brand.activateBtn");
|
|
131
|
-
btn.setAttribute("data-i18n", "skills.brand.activateBtn");
|
|
132
|
-
btn.addEventListener("click", () => {
|
|
133
|
-
if (typeof Brand !== "undefined" && Brand.goToLicenseInput) {
|
|
134
|
-
Brand.goToLicenseInput();
|
|
135
|
-
} else {
|
|
136
|
-
Router.navigate("settings");
|
|
137
|
-
}
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
hint.appendChild(msgEl);
|
|
141
|
-
hint.appendChild(btn);
|
|
142
|
-
container.appendChild(hint);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/** Render a single brand skill card. */
|
|
147
|
-
function _renderBrandSkillCard(skill) {
|
|
148
|
-
const name = skill.name;
|
|
149
|
-
const installedVersion = skill.installed_version;
|
|
150
|
-
const latestVersion = (skill.latest_version || {}).version || skill.version;
|
|
151
|
-
const needsUpdate = skill.needs_update;
|
|
152
|
-
|
|
153
|
-
// Determine action badge
|
|
154
|
-
let statusHtml = "";
|
|
155
|
-
if (!installedVersion) {
|
|
156
|
-
const versionBadge = latestVersion
|
|
157
|
-
? `<span class="brand-skill-version latest">v${escapeHtml(latestVersion)}</span>` : "";
|
|
158
|
-
statusHtml = `${versionBadge}<button class="btn-brand-install" data-name="${escapeHtml(name)}">${I18n.t("skills.brand.btn.install")}</button>`;
|
|
159
|
-
} else if (needsUpdate) {
|
|
160
|
-
statusHtml = `
|
|
161
|
-
<span class="brand-skill-version installed">v${escapeHtml(installedVersion)}</span>
|
|
162
|
-
<span class="brand-skill-update-arrow">→</span>
|
|
163
|
-
<span class="brand-skill-version latest">v${escapeHtml(latestVersion)}</span>
|
|
164
|
-
<button class="btn-brand-update" data-name="${escapeHtml(name)}">${I18n.t("skills.brand.btn.update")}</button>`;
|
|
165
|
-
} else {
|
|
166
|
-
// Installed and up-to-date — show version badge + "Use" button
|
|
167
|
-
const displayVersion = installedVersion || latestVersion;
|
|
168
|
-
statusHtml = `
|
|
169
|
-
<span class="brand-skill-version installed">v${escapeHtml(displayVersion)} ✓</span>
|
|
170
|
-
<button class="btn-brand-use" data-name="${escapeHtml(name)}">${I18n.t("skills.brand.btn.use")}</button>`;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// Free skills show a "Free" badge; paid (encrypted) brand skills show "Private".
|
|
174
|
-
const badge = skill.is_free
|
|
175
|
-
? `<span class="brand-skill-badge-free" title="${I18n.t("skills.brand.freeTip")}">✨ ${I18n.t("skills.brand.free")}</span>`
|
|
176
|
-
: `<span class="brand-skill-badge-private" title="${I18n.t("skills.brand.privateTip")}">🔒 ${I18n.t("skills.brand.private")}</span>`;
|
|
177
|
-
|
|
178
|
-
// Choose description based on current language
|
|
179
|
-
const currentLang = I18n.lang();
|
|
180
|
-
const description = (currentLang === "zh" && skill.description_zh)
|
|
181
|
-
? skill.description_zh
|
|
182
|
-
: skill.description || "";
|
|
183
|
-
|
|
184
|
-
const card = document.createElement("div");
|
|
185
|
-
card.className = "brand-skill-card";
|
|
186
|
-
card.innerHTML = `
|
|
187
|
-
<div class="brand-skill-card-main">
|
|
188
|
-
<div class="brand-skill-info">
|
|
189
|
-
<div class="brand-skill-title">
|
|
190
|
-
<span class="brand-skill-name">${escapeHtml((currentLang === "zh" && skill.name_zh) ? skill.name_zh : name)}</span>
|
|
191
|
-
${badge}
|
|
192
|
-
</div>
|
|
193
|
-
<div class="brand-skill-desc">${escapeHtml(description)}</div>
|
|
194
|
-
</div>
|
|
195
|
-
<div class="brand-skill-actions">${statusHtml}</div>
|
|
196
|
-
</div>`;
|
|
197
|
-
|
|
198
|
-
// Bind install/update/use buttons
|
|
199
|
-
const installBtn = card.querySelector(".btn-brand-install");
|
|
200
|
-
const updateBtn = card.querySelector(".btn-brand-update");
|
|
201
|
-
const useBtn = card.querySelector(".btn-brand-use");
|
|
202
|
-
if (installBtn) installBtn.addEventListener("click", () => _installBrandSkill(name, installBtn));
|
|
203
|
-
if (updateBtn) updateBtn.addEventListener("click", () => _installBrandSkill(name, updateBtn));
|
|
204
|
-
if (useBtn) useBtn.addEventListener("click", () => _useInstalledSkill(name));
|
|
205
|
-
|
|
206
|
-
return card;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/** Show a temporary inline error message below `btn`, auto-dismiss after 5 s. */
|
|
210
|
-
function _showBrandInstallError(btn, message) {
|
|
211
|
-
// Remove any existing error tip on this button's parent
|
|
212
|
-
const existing = btn.parentElement.querySelector(".brand-install-error");
|
|
213
|
-
if (existing) existing.remove();
|
|
214
|
-
|
|
215
|
-
const tip = document.createElement("div");
|
|
216
|
-
tip.className = "brand-install-error";
|
|
217
|
-
tip.textContent = message;
|
|
218
|
-
btn.parentElement.appendChild(tip);
|
|
219
|
-
setTimeout(() => tip.remove(), 5000);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/** Return a user-friendly message for install/update errors. */
|
|
223
|
-
function _friendlyInstallError(rawError) {
|
|
224
|
-
if (!rawError) return I18n.t("skills.brand.unknownError");
|
|
225
|
-
const lower = rawError.toLowerCase();
|
|
226
|
-
if (lower.includes("timeout") || lower.includes("network error") ||
|
|
227
|
-
lower.includes("execution expired") || lower.includes("failed to open")) {
|
|
228
|
-
return I18n.t("skills.brand.networkRetry");
|
|
229
|
-
}
|
|
230
|
-
return I18n.t("skills.brand.installFailed") + rawError;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
/** Install or update a brand skill. */
|
|
234
|
-
async function _installBrandSkill(name, btn) {
|
|
235
|
-
const originalText = btn.textContent;
|
|
236
|
-
btn.disabled = true;
|
|
237
|
-
btn.textContent = I18n.t("skills.brand.btn.installing");
|
|
238
|
-
|
|
239
|
-
try {
|
|
240
|
-
const res = await fetch(`/api/brand/skills/${encodeURIComponent(name)}/install`, { method: "POST" });
|
|
241
|
-
const data = await res.json();
|
|
242
|
-
|
|
243
|
-
if (!res.ok || !data.ok) {
|
|
244
|
-
_showBrandInstallError(btn, _friendlyInstallError(data.error));
|
|
245
|
-
btn.disabled = false;
|
|
246
|
-
btn.textContent = originalText;
|
|
247
|
-
return;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// Update local state to reflect installed version
|
|
251
|
-
const skill = _brandSkills.find(s => s.name === name);
|
|
252
|
-
if (skill) {
|
|
253
|
-
skill.installed_version = data.version;
|
|
254
|
-
skill.needs_update = false;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// Re-render brand skills tab
|
|
258
|
-
_renderBrandSkills();
|
|
259
|
-
|
|
260
|
-
// Also reload My Skills — the new skill may appear there now
|
|
261
|
-
await Skills.load();
|
|
262
|
-
} catch (e) {
|
|
263
|
-
_showBrandInstallError(btn, I18n.t("skills.brand.networkRetry"));
|
|
264
|
-
btn.disabled = false;
|
|
265
|
-
btn.textContent = originalText;
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/** Open a new session and trigger a brand skill by sending "/{name}" as the first message. */
|
|
270
|
-
async function _useInstalledSkill(name) {
|
|
271
|
-
const maxN = Sessions.all.reduce((max, s) => {
|
|
272
|
-
const m = s.name.match(/^Session (\d+)$/);
|
|
273
|
-
return m ? Math.max(max, parseInt(m[1], 10)) : max;
|
|
274
|
-
}, 0);
|
|
275
|
-
const res = await fetch("/api/sessions", {
|
|
276
|
-
method: "POST",
|
|
277
|
-
headers: { "Content-Type": "application/json" },
|
|
278
|
-
body: JSON.stringify({ name: "Session " + (maxN + 1), source: "manual" })
|
|
279
|
-
});
|
|
280
|
-
const data = await res.json();
|
|
281
|
-
if (!res.ok) { alert(I18n.t("tasks.sessionError") + (data.error || "unknown")); return; }
|
|
282
|
-
|
|
283
|
-
const session = data.session;
|
|
284
|
-
if (!session) return;
|
|
285
|
-
|
|
286
|
-
if (!WS.ready) {
|
|
287
|
-
WS.connect();
|
|
288
|
-
Skills.load();
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
Sessions.add(session);
|
|
292
|
-
Sessions.renderList();
|
|
293
|
-
Sessions.setPendingMessage(session.id, "/" + name);
|
|
294
|
-
Sessions.select(session.id);
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
/** Render a single skill card in My Skills tab. */
|
|
298
|
-
function _renderSkillCard(skill) {
|
|
299
|
-
const card = document.createElement("div");
|
|
300
|
-
// invalid = unrecoverable (can't be used at all); warning = auto-corrected but fully usable
|
|
301
|
-
card.className = "skill-card" + (skill.invalid ? " skill-card-invalid" : "");
|
|
302
|
-
|
|
303
|
-
// "default" = built-in gem skills; "brand" = encrypted brand/system skills
|
|
304
|
-
const isSystem = skill.source === "default" || skill.source === "brand";
|
|
305
|
-
const badgeClass = isSystem ? "skill-badge skill-badge-system" : "skill-badge skill-badge-custom";
|
|
306
|
-
const badgeLabel = isSystem ? I18n.t("skills.badge.system") : I18n.t("skills.badge.custom");
|
|
307
|
-
|
|
308
|
-
// Build warning icon for skills with auto-corrected issues (still fully usable)
|
|
309
|
-
// Build error notice for truly invalid skills (can't be used)
|
|
310
|
-
let warnIconHtml = "";
|
|
311
|
-
let errorNoticeHtml = "";
|
|
312
|
-
if (skill.invalid) {
|
|
313
|
-
const reason = skill.invalid_reason || I18n.t("skills.invalid.reason");
|
|
314
|
-
errorNoticeHtml = `<div class="skill-notice skill-notice-error">⚠ ${escapeHtml(reason)}</div>`;
|
|
315
|
-
} else if (skill.warnings && skill.warnings.length > 0) {
|
|
316
|
-
const reason = skill.warnings.join("\n");
|
|
317
|
-
const tooltip = I18n.t("skills.warning.tooltip", { reason });
|
|
318
|
-
warnIconHtml = `<span class="skill-warn-icon" data-tooltip="${escapeHtml(tooltip)}">⚠</span>`;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
// toggle is only disabled for system skills or truly invalid ones; warning skills are fine
|
|
322
|
-
const toggleDisabled = isSystem || skill.invalid;
|
|
323
|
-
const toggleTitle = isSystem ? I18n.t("skills.systemDisabledTip")
|
|
324
|
-
: skill.invalid ? I18n.t("skills.invalid.toggleTip")
|
|
325
|
-
: skill.enabled ? I18n.t("skills.toggle.disableDesc")
|
|
326
|
-
: I18n.t("skills.toggle.enableDesc");
|
|
327
|
-
|
|
328
|
-
// Choose description based on current language
|
|
329
|
-
const currentLang = I18n.lang();
|
|
330
|
-
const description = (currentLang === "zh" && skill.description_zh)
|
|
331
|
-
? skill.description_zh
|
|
332
|
-
: skill.description || "";
|
|
333
|
-
|
|
334
|
-
// Show "Use" button for all skills except invalid ones
|
|
335
|
-
const useButtonHtml = skill.invalid
|
|
336
|
-
? ""
|
|
337
|
-
: `<button class="btn-skill-use" data-name="${escapeHtml(skill.name)}">${I18n.t("skills.btn.use")}</button>`;
|
|
338
|
-
|
|
339
|
-
// Delete button only for custom (non-system, non-brand) skills
|
|
340
|
-
const deleteButtonHtml = isSystem
|
|
341
|
-
? ""
|
|
342
|
-
: `<button class="btn-skill-delete" data-name="${escapeHtml(skill.name)}" title="${I18n.t("skills.btn.delete")}">
|
|
343
|
-
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
344
|
-
<polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/>
|
|
345
|
-
</svg>
|
|
346
|
-
</button>`;
|
|
347
|
-
|
|
348
|
-
card.innerHTML = `
|
|
349
|
-
<div class="skill-card-main">
|
|
350
|
-
<div class="skill-card-info">
|
|
351
|
-
<div class="skill-card-title">
|
|
352
|
-
${warnIconHtml}
|
|
353
|
-
<span class="skill-name">${escapeHtml((currentLang === "zh" && skill.name_zh) ? skill.name_zh : skill.name)}</span>
|
|
354
|
-
<span class="${badgeClass}">${badgeLabel}</span>
|
|
355
|
-
${skill.invalid ? `<span class="skill-badge skill-badge-invalid">${I18n.t("skills.badge.invalid")}</span>` : ""}
|
|
356
|
-
</div>
|
|
357
|
-
<div class="skill-card-desc">${escapeHtml(description)}</div>
|
|
358
|
-
</div>
|
|
359
|
-
<div class="skill-card-actions">
|
|
360
|
-
<label class="skill-toggle ${toggleDisabled ? "skill-toggle-disabled" : ""}" data-tooltip="${escapeHtml(toggleTitle)}">
|
|
361
|
-
<input type="checkbox" class="skill-toggle-input" ${skill.enabled ? "checked" : ""} ${toggleDisabled ? "disabled" : ""}>
|
|
362
|
-
<span class="skill-toggle-track"></span>
|
|
363
|
-
</label>
|
|
364
|
-
${useButtonHtml}
|
|
365
|
-
${deleteButtonHtml}
|
|
366
|
-
</div>
|
|
367
|
-
</div>
|
|
368
|
-
${errorNoticeHtml}`;
|
|
369
|
-
|
|
370
|
-
// Bind toggle event
|
|
371
|
-
if (!isSystem) {
|
|
372
|
-
const checkbox = card.querySelector(".skill-toggle-input");
|
|
373
|
-
checkbox.addEventListener("change", async () => {
|
|
374
|
-
await Skills.toggle(skill.name, checkbox.checked);
|
|
375
|
-
});
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
// Flip tooltip below when toggle is near top of scroll container
|
|
379
|
-
const toggleLabel = card.querySelector(".skill-toggle");
|
|
380
|
-
if (toggleLabel) {
|
|
381
|
-
toggleLabel.addEventListener("mouseenter", () => {
|
|
382
|
-
const scroller = toggleLabel.closest(".skills-tab-content");
|
|
383
|
-
if (!scroller) return;
|
|
384
|
-
const toggleTop = toggleLabel.getBoundingClientRect().top;
|
|
385
|
-
const scrollerTop = scroller.getBoundingClientRect().top;
|
|
386
|
-
if (toggleTop - scrollerTop < 80) {
|
|
387
|
-
toggleLabel.setAttribute("data-tooltip-pos", "bottom");
|
|
388
|
-
}
|
|
389
|
-
});
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
// Bind "Use" button event
|
|
393
|
-
const useBtn = card.querySelector(".btn-skill-use");
|
|
394
|
-
if (useBtn) {
|
|
395
|
-
useBtn.addEventListener("click", () => _useInstalledSkill(skill.name));
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
// Bind delete button event
|
|
399
|
-
const deleteBtn = card.querySelector(".btn-skill-delete");
|
|
400
|
-
if (deleteBtn) {
|
|
401
|
-
deleteBtn.addEventListener("click", (e) => {
|
|
402
|
-
e.stopPropagation();
|
|
403
|
-
Skills.delete(skill.name);
|
|
404
|
-
});
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
return card;
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
/** Render My Skills tab content. */
|
|
411
|
-
function _renderMySkills() {
|
|
412
|
-
const container = $("skills-list");
|
|
413
|
-
console.log("[Skills] _renderMySkills, container=", container, "_skills.length=", _skills.length);
|
|
414
|
-
if (!container) { console.error("[Skills] skills-list not found!"); return; }
|
|
415
|
-
container.innerHTML = "";
|
|
416
|
-
|
|
417
|
-
// Optionally hide system (source=default) skills that aren't marked always_show
|
|
418
|
-
const visible = _showSystemSkills
|
|
419
|
-
? _skills
|
|
420
|
-
: _skills.filter(s => s.always_show || s.source !== "default");
|
|
421
|
-
|
|
422
|
-
if (visible.length === 0) {
|
|
423
|
-
const emptyText = I18n.t("skills.empty");
|
|
424
|
-
const createBtnText = I18n.t("skills.empty.createBtn");
|
|
425
|
-
|
|
426
|
-
const emptyWrapper = document.createElement("div");
|
|
427
|
-
emptyWrapper.className = "skills-empty";
|
|
428
|
-
|
|
429
|
-
const emptyTextEl = document.createElement("div");
|
|
430
|
-
emptyTextEl.className = "skills-empty-text";
|
|
431
|
-
emptyTextEl.textContent = emptyText;
|
|
432
|
-
|
|
433
|
-
const createBtn = document.createElement("div");
|
|
434
|
-
createBtn.className = "skills-empty-create-btn";
|
|
435
|
-
createBtn.innerHTML = `
|
|
436
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
437
|
-
<path d="M12 2a10 10 0 1 0 10 10A10 10 0 0 0 12 2z"/><path d="M12 8v8"/><path d="M8 12h8"/>
|
|
438
|
-
</svg>
|
|
439
|
-
<span>${escapeHtml(createBtnText)}</span>
|
|
440
|
-
<svg class="skills-empty-create-arrow" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
441
|
-
<path d="M5 12h14"/><path d="M12 5l7 7-7 7"/>
|
|
442
|
-
</svg>`;
|
|
443
|
-
createBtn.addEventListener("click", () => Skills.createInSession("/skill-creator"));
|
|
444
|
-
|
|
445
|
-
emptyWrapper.appendChild(emptyTextEl);
|
|
446
|
-
emptyWrapper.appendChild(createBtn);
|
|
447
|
-
container.appendChild(emptyWrapper);
|
|
448
|
-
} else {
|
|
449
|
-
// System skills first, then custom
|
|
450
|
-
const sorted = [
|
|
451
|
-
...visible.filter(s => s.source === "default"),
|
|
452
|
-
...visible.filter(s => s.source !== "default")
|
|
453
|
-
];
|
|
454
|
-
sorted.forEach((skill, i) => {
|
|
455
|
-
try {
|
|
456
|
-
container.appendChild(_renderSkillCard(skill));
|
|
457
|
-
} catch (e) {
|
|
458
|
-
console.error("[Skills] _renderSkillCard failed for skill", i, skill.name, e);
|
|
459
|
-
}
|
|
460
|
-
});
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
// ── Public API ─────────────────────────────────────────────────────────
|
|
465
|
-
return {
|
|
466
|
-
|
|
467
|
-
// ── Data ─────────────────────────────────────────────────────────────
|
|
468
|
-
|
|
469
|
-
/** Return current skills list (read-only snapshot). */
|
|
470
|
-
get all() { return _skills.slice(); },
|
|
471
|
-
|
|
472
|
-
/** Fetch skills from server; re-render sidebar + panel if open. */
|
|
473
|
-
async load() {
|
|
474
|
-
try {
|
|
475
|
-
const res = await fetch("/api/skills");
|
|
476
|
-
const data = await res.json();
|
|
477
|
-
_skills = data.skills || [];
|
|
478
|
-
Skills.renderSection();
|
|
479
|
-
if (Router.current === "skills") {
|
|
480
|
-
try {
|
|
481
|
-
_renderMySkills();
|
|
482
|
-
} catch (renderErr) {
|
|
483
|
-
console.error("[Skills] _renderMySkills failed", renderErr);
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
} catch (e) {
|
|
487
|
-
console.error("[Skills] load failed", e);
|
|
488
|
-
}
|
|
489
|
-
},
|
|
490
|
-
|
|
491
|
-
// ── Router interface ──────────────────────────────────────────────────
|
|
492
|
-
|
|
493
|
-
/** Called by Router when the skills panel becomes active. */
|
|
494
|
-
onPanelShow() {
|
|
495
|
-
// ── One-time DOM wiring ──────────────────────────────────────────────
|
|
496
|
-
// Bind tab clicks here (not in the IIFE) because $ and the DOM elements
|
|
497
|
-
// are only guaranteed to exist after app.js has loaded and the panel
|
|
498
|
-
// has been shown at least once. Guard with _domWired so we only do this
|
|
499
|
-
// once no matter how many times the user navigates to the Skills panel.
|
|
500
|
-
if (!_domWired) {
|
|
501
|
-
document.querySelectorAll(".skills-tab").forEach(btn => {
|
|
502
|
-
btn.addEventListener("click", () => _switchTab(btn.dataset.tab));
|
|
503
|
-
});
|
|
504
|
-
|
|
505
|
-
const refreshBtn = $("btn-refresh-brand-skills");
|
|
506
|
-
if (refreshBtn) {
|
|
507
|
-
refreshBtn.addEventListener("click", async () => {
|
|
508
|
-
// Add spinning animation
|
|
509
|
-
const icon = refreshBtn.querySelector("svg");
|
|
510
|
-
if (icon) {
|
|
511
|
-
icon.classList.add("spinning");
|
|
512
|
-
}
|
|
513
|
-
refreshBtn.disabled = true;
|
|
514
|
-
|
|
515
|
-
_brandSkills = [];
|
|
516
|
-
await _loadBrandSkills();
|
|
517
|
-
|
|
518
|
-
// Remove spinning animation
|
|
519
|
-
if (icon) {
|
|
520
|
-
icon.classList.remove("spinning");
|
|
521
|
-
}
|
|
522
|
-
refreshBtn.disabled = false;
|
|
523
|
-
});
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
// Wire the "show system skills" checkbox
|
|
527
|
-
const chkSystem = $("chk-show-system-skills");
|
|
528
|
-
if (chkSystem) {
|
|
529
|
-
chkSystem.checked = _showSystemSkills;
|
|
530
|
-
chkSystem.addEventListener("change", () => {
|
|
531
|
-
_showSystemSkills = chkSystem.checked;
|
|
532
|
-
_renderMySkills();
|
|
533
|
-
});
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
// Re-render skill cards when the user switches language
|
|
537
|
-
document.addEventListener("langchange", () => {
|
|
538
|
-
_renderMySkills();
|
|
539
|
-
_renderBrandSkills();
|
|
540
|
-
});
|
|
541
|
-
|
|
542
|
-
_domWired = true;
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
_renderMySkills();
|
|
546
|
-
Skills.renderSection();
|
|
547
|
-
|
|
548
|
-
// Restore active tab state immediately
|
|
549
|
-
_switchTab(_activeTab);
|
|
550
|
-
|
|
551
|
-
// Async: check brand license status and update Brand Skills tab visibility.
|
|
552
|
-
fetch("/api/brand/status")
|
|
553
|
-
.then(res => res.json())
|
|
554
|
-
.then(data => {
|
|
555
|
-
const prevActivated = _brandActivated;
|
|
556
|
-
|
|
557
|
-
_brandActivated = data.branded && !data.needs_activation;
|
|
558
|
-
|
|
559
|
-
// Show the Brand Skills tab for any branded project, even without an active
|
|
560
|
-
// license — the tab itself will show an activation prompt in that case.
|
|
561
|
-
const brandTab = $("tab-brand-skills");
|
|
562
|
-
if (brandTab) brandTab.style.display = data.branded ? "" : "none";
|
|
563
|
-
|
|
564
|
-
// Re-render my-skills tab if brand activated status changed.
|
|
565
|
-
if (prevActivated !== _brandActivated) {
|
|
566
|
-
_renderMySkills();
|
|
567
|
-
}
|
|
568
|
-
})
|
|
569
|
-
.catch(() => {
|
|
570
|
-
// On network error, keep whatever is currently shown
|
|
571
|
-
});
|
|
572
|
-
},
|
|
573
|
-
|
|
574
|
-
// ── Sidebar rendering ─────────────────────────────────────────────────
|
|
575
|
-
|
|
576
|
-
renderSection() {
|
|
577
|
-
// Sidebar item is static in HTML — just update the label text.
|
|
578
|
-
const labelEl = $("skills-sidebar-label");
|
|
579
|
-
if (!labelEl) return;
|
|
580
|
-
labelEl.textContent = I18n.t("sidebar.skills");
|
|
581
|
-
},
|
|
582
|
-
|
|
583
|
-
// ── Actions ───────────────────────────────────────────────────────────
|
|
584
|
-
|
|
585
|
-
/** Toggle enable/disable for a skill. */
|
|
586
|
-
async toggle(name, enabled) {
|
|
587
|
-
try {
|
|
588
|
-
const res = await fetch(`/api/skills/${encodeURIComponent(name)}/toggle`, {
|
|
589
|
-
method: "PATCH",
|
|
590
|
-
headers: { "Content-Type": "application/json" },
|
|
591
|
-
body: JSON.stringify({ enabled })
|
|
592
|
-
});
|
|
593
|
-
const data = await res.json();
|
|
594
|
-
if (!res.ok) { alert(I18n.t("skills.toggleError") + (data.error || "unknown")); return; }
|
|
595
|
-
await Skills.load();
|
|
596
|
-
} catch (e) {
|
|
597
|
-
console.error("[Skills] toggle failed", e);
|
|
598
|
-
}
|
|
599
|
-
},
|
|
600
|
-
|
|
601
|
-
/** Delete a skill by name. Prompts confirmation, then DELETE to server. */
|
|
602
|
-
async delete(name) {
|
|
603
|
-
if (!confirm(I18n.t("skills.deleteConfirm", { name }))) return;
|
|
604
|
-
try {
|
|
605
|
-
const res = await fetch(`/api/skills/${encodeURIComponent(name)}`, { method: "DELETE" });
|
|
606
|
-
const data = await res.json();
|
|
607
|
-
if (!res.ok) { alert(data.error || I18n.t("skills.deleteError")); return; }
|
|
608
|
-
Modal.toast(I18n.t("skills.deleted", { name }), "success");
|
|
609
|
-
await Skills.load();
|
|
610
|
-
} catch (e) {
|
|
611
|
-
console.error("[Skills] delete failed", e);
|
|
612
|
-
}
|
|
613
|
-
},
|
|
614
|
-
|
|
615
|
-
/** Switch the Skills panel to the brand-skills tab.
|
|
616
|
-
* Called externally (e.g. from settings.js after license activation) to
|
|
617
|
-
* guide the user directly to the Brand Skills download page.
|
|
618
|
-
* Ensures DOM is wired and forces a fresh load of brand skills.
|
|
619
|
-
*/
|
|
620
|
-
openBrandSkillsTab() {
|
|
621
|
-
// Make sure the panel DOM listeners are wired before switching tabs
|
|
622
|
-
Skills.onPanelShow();
|
|
623
|
-
// Force reload brand skills (activation may have just happened)
|
|
624
|
-
_brandSkills = [];
|
|
625
|
-
_switchTab("brand-skills");
|
|
626
|
-
},
|
|
627
|
-
|
|
628
|
-
/** Reset the skills panel back to My Skills tab and clear brand data.
|
|
629
|
-
* Called after license unbind so the user is not left on Brand Skills tab.
|
|
630
|
-
*/
|
|
631
|
-
resetAfterUnbind() {
|
|
632
|
-
_brandSkills = [];
|
|
633
|
-
_brandActivated = false;
|
|
634
|
-
_activeTab = "my-skills";
|
|
635
|
-
// Hide the Brand Skills tab since there is no active license
|
|
636
|
-
const brandTab = $("tab-brand-skills");
|
|
637
|
-
if (brandTab) brandTab.style.display = "none";
|
|
638
|
-
// If the panel is currently visible, switch to My Skills immediately
|
|
639
|
-
const panel = $("skills-panel");
|
|
640
|
-
if (panel && panel.style.display !== "none") {
|
|
641
|
-
_switchTab("my-skills");
|
|
642
|
-
}
|
|
643
|
-
},
|
|
644
|
-
|
|
645
|
-
// ── Import bar ────────────────────────────────────────────────────────
|
|
646
|
-
|
|
647
|
-
/** Toggle the inline import bar below the My Skills header.
|
|
648
|
-
* Switches to "my-skills" tab first so the bar is visible.
|
|
649
|
-
* Wires confirm / cancel / Enter key handlers on first call.
|
|
650
|
-
*/
|
|
651
|
-
toggleImportBar() {
|
|
652
|
-
// Always switch to My Skills tab so the import bar appears in context
|
|
653
|
-
_switchTab("my-skills");
|
|
654
|
-
|
|
655
|
-
const bar = $("skill-import-bar");
|
|
656
|
-
const input = $("skill-import-input");
|
|
657
|
-
const confirmBtn = $("btn-skill-import-confirm");
|
|
658
|
-
const cancelBtn = $("btn-skill-import-cancel");
|
|
659
|
-
if (!bar) return;
|
|
660
|
-
|
|
661
|
-
const isOpen = bar.style.display !== "none";
|
|
662
|
-
|
|
663
|
-
if (isOpen) {
|
|
664
|
-
// Close the bar
|
|
665
|
-
bar.style.display = "none";
|
|
666
|
-
if (input) input.value = "";
|
|
667
|
-
return;
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
// Open the bar
|
|
671
|
-
bar.style.display = "";
|
|
672
|
-
if (input) {
|
|
673
|
-
input.focus();
|
|
674
|
-
input.placeholder = I18n.t("skills.import.placeholder");
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
// Wire one-time listeners (guard with dataset flag)
|
|
678
|
-
if (!bar.dataset.wired) {
|
|
679
|
-
bar.dataset.wired = "1";
|
|
680
|
-
|
|
681
|
-
// Confirm button
|
|
682
|
-
confirmBtn.addEventListener("click", () => Skills._doImportFromBar());
|
|
683
|
-
|
|
684
|
-
// Enter key in input
|
|
685
|
-
input.addEventListener("keydown", (e) => {
|
|
686
|
-
if (e.key === "Enter") { e.preventDefault(); Skills._doImportFromBar(); }
|
|
687
|
-
});
|
|
688
|
-
|
|
689
|
-
// Cancel button
|
|
690
|
-
cancelBtn.addEventListener("click", () => {
|
|
691
|
-
bar.style.display = "none";
|
|
692
|
-
input.value = "";
|
|
693
|
-
});
|
|
694
|
-
|
|
695
|
-
// Browse button — open system file picker, upload zip, fill path into input
|
|
696
|
-
const browseBtn = $("btn-skill-import-browse");
|
|
697
|
-
const fileInput = $("skill-import-file");
|
|
698
|
-
if (browseBtn && fileInput) {
|
|
699
|
-
browseBtn.addEventListener("click", () => fileInput.click());
|
|
700
|
-
fileInput.addEventListener("change", async () => {
|
|
701
|
-
const file = fileInput.files[0];
|
|
702
|
-
if (!file) return;
|
|
703
|
-
|
|
704
|
-
// Show filename immediately so the user sees feedback
|
|
705
|
-
input.value = file.name;
|
|
706
|
-
input.placeholder = "";
|
|
707
|
-
browseBtn.disabled = true;
|
|
708
|
-
browseBtn.style.opacity = "0.5";
|
|
709
|
-
|
|
710
|
-
try {
|
|
711
|
-
const form = new FormData();
|
|
712
|
-
form.append("file", file);
|
|
713
|
-
const res = await fetch("/api/upload", { method: "POST", body: form });
|
|
714
|
-
const data = await res.json();
|
|
715
|
-
if (res.ok && data.path) {
|
|
716
|
-
// Fill the server-side temp path — /skill-add will read it directly
|
|
717
|
-
input.value = data.path;
|
|
718
|
-
} else {
|
|
719
|
-
input.value = "";
|
|
720
|
-
alert(data.error || "Upload failed");
|
|
721
|
-
}
|
|
722
|
-
} catch (e) {
|
|
723
|
-
input.value = "";
|
|
724
|
-
console.error("[Skills] upload error", e);
|
|
725
|
-
} finally {
|
|
726
|
-
browseBtn.disabled = false;
|
|
727
|
-
browseBtn.style.opacity = "";
|
|
728
|
-
// Reset file input so the same file can be picked again if needed
|
|
729
|
-
fileInput.value = "";
|
|
730
|
-
}
|
|
731
|
-
});
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
},
|
|
735
|
-
|
|
736
|
-
/** Execute import: validate URL, open a session and send /skill-add <url>. */
|
|
737
|
-
async _doImportFromBar() {
|
|
738
|
-
const input = $("skill-import-input");
|
|
739
|
-
const bar = $("skill-import-bar");
|
|
740
|
-
const url = (input ? input.value : "").trim();
|
|
741
|
-
|
|
742
|
-
if (!url) {
|
|
743
|
-
input && input.focus();
|
|
744
|
-
return;
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
// Validate: accept http(s) URLs or absolute local paths (from upload)
|
|
748
|
-
const isUrl = /^https?:\/\//i.test(url);
|
|
749
|
-
const isLocalPath = url.startsWith("/") || url.startsWith("~");
|
|
750
|
-
if (!isUrl && !isLocalPath) {
|
|
751
|
-
input.classList.add("skill-import-input-error");
|
|
752
|
-
setTimeout(() => input.classList.remove("skill-import-input-error"), 1200);
|
|
753
|
-
input.focus();
|
|
754
|
-
return;
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
// Close the bar immediately — the session takes over from here
|
|
758
|
-
if (bar) bar.style.display = "none";
|
|
759
|
-
if (input) input.value = "";
|
|
760
|
-
|
|
761
|
-
// Create a new session and queue the /skill-add command
|
|
762
|
-
try {
|
|
763
|
-
const maxN = Sessions.all.reduce((max, s) => {
|
|
764
|
-
const m = s.name.match(/^Session (\d+)$/);
|
|
765
|
-
return m ? Math.max(max, parseInt(m[1], 10)) : max;
|
|
766
|
-
}, 0);
|
|
767
|
-
const res = await fetch("/api/sessions", {
|
|
768
|
-
method: "POST",
|
|
769
|
-
headers: { "Content-Type": "application/json" },
|
|
770
|
-
body: JSON.stringify({ name: "Session " + (maxN + 1), source: "manual" })
|
|
771
|
-
});
|
|
772
|
-
const data = await res.json();
|
|
773
|
-
if (!res.ok) { alert(I18n.t("tasks.sessionError") + (data.error || "unknown")); return; }
|
|
774
|
-
|
|
775
|
-
const session = data.session;
|
|
776
|
-
if (!session) return;
|
|
777
|
-
|
|
778
|
-
if (!WS.ready) { WS.connect(); Tasks.load(); }
|
|
779
|
-
|
|
780
|
-
Sessions.add(session);
|
|
781
|
-
Sessions.renderList();
|
|
782
|
-
Sessions.setPendingMessage(session.id, `/skill-add ${url}`);
|
|
783
|
-
Sessions.select(session.id);
|
|
784
|
-
} catch (e) {
|
|
785
|
-
console.error("[Skills] import failed", e);
|
|
786
|
-
alert(I18n.lang() === "zh" ? "导入技能时网络错误。" : "Network error while importing skill.");
|
|
787
|
-
}
|
|
788
|
-
},
|
|
789
|
-
|
|
790
|
-
/** Create a new custom skill by opening a session and sending /skill-creator. */
|
|
791
|
-
async createInSession(message) {
|
|
792
|
-
const maxN = Sessions.all.reduce((max, s) => {
|
|
793
|
-
const m = s.name.match(/^Session (\d+)$/);
|
|
794
|
-
return m ? Math.max(max, parseInt(m[1], 10)) : max;
|
|
795
|
-
}, 0);
|
|
796
|
-
const res = await fetch("/api/sessions", {
|
|
797
|
-
method: "POST",
|
|
798
|
-
headers: { "Content-Type": "application/json" },
|
|
799
|
-
body: JSON.stringify({ name: "Session " + (maxN + 1), source: "manual" })
|
|
800
|
-
});
|
|
801
|
-
const data = await res.json();
|
|
802
|
-
if (!res.ok) { alert(I18n.t("tasks.sessionError") + (data.error || "unknown")); return; }
|
|
803
|
-
|
|
804
|
-
const session = data.session;
|
|
805
|
-
if (!session) return;
|
|
806
|
-
|
|
807
|
-
// If WS is not yet connected (e.g. called during onboarding), boot the UI
|
|
808
|
-
// first so WS connects, then use setPendingMessage so the command is sent
|
|
809
|
-
// once the socket is ready.
|
|
810
|
-
if (!WS.ready) {
|
|
811
|
-
WS.connect();
|
|
812
|
-
Tasks.load();
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
Sessions.add(session);
|
|
816
|
-
Sessions.renderList();
|
|
817
|
-
Sessions.setPendingMessage(session.id, message || "/skill-creator");
|
|
818
|
-
Sessions.select(session.id);
|
|
819
|
-
},
|
|
820
|
-
};
|
|
821
|
-
})();
|
|
1
|
+
// ── SkillAC — slash-command autocomplete + composer bindings ──────────────
|
|
2
|
+
// NOTE: The Skills data/render module moved to features/skills/{store,view}.js.
|
|
3
|
+
// Only the composer-side autocomplete lives here.
|
|
822
4
|
|
|
823
5
|
// ─────────────────────────────────────────────────────────────────────────
|
|
824
6
|
// SkillAC — slash-command skill autocomplete dropdown + composer bindings
|