openclacky 1.5.4 → 1.5.6
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 +63 -0
- data/lib/clacky/agent/goal_manager.rb +305 -0
- data/lib/clacky/agent/goal_state.rb +88 -0
- data/lib/clacky/agent/message_compressor_helper.rb +4 -1
- data/lib/clacky/agent/session_serializer.rb +27 -3
- data/lib/clacky/agent.rb +202 -3
- data/lib/clacky/agent_config.rb +4 -2
- data/lib/clacky/agent_profile.rb +25 -3
- data/lib/clacky/cli.rb +15 -15
- data/lib/clacky/default_extensions/ext-studio/api/handler.rb +29 -1
- data/lib/clacky/default_extensions/ext-studio/ext.yml +4 -4
- data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +130 -59
- data/lib/clacky/default_extensions/ext-studio/skills/ext-develop/SKILL.md +12 -4
- data/lib/clacky/default_extensions/git/ext.yml +3 -0
- data/lib/clacky/default_extensions/meeting/ext.yml +3 -0
- data/lib/clacky/default_extensions/time_machine/ext.yml +4 -0
- data/lib/clacky/default_searchers/tavily.rb +98 -0
- data/lib/clacky/default_skills/new/SKILL.md +17 -12
- data/lib/clacky/extension/api_extension.rb +29 -1
- data/lib/clacky/extension/loader.rb +1 -0
- data/lib/clacky/extension/packager.rb +114 -20
- data/lib/clacky/extension/verifier.rb +1 -1
- data/lib/clacky/idle_compression_timer.rb +1 -3
- data/lib/clacky/message_format/anthropic.rb +1 -1
- data/lib/clacky/message_format/bedrock.rb +1 -1
- data/lib/clacky/message_format/open_ai.rb +8 -0
- data/lib/clacky/providers.rb +65 -8
- data/lib/clacky/search_config.rb +57 -0
- data/lib/clacky/server/http_server.rb +249 -31
- data/lib/clacky/server/session_registry.rb +10 -3
- data/lib/clacky/server/web_ui_controller.rb +12 -2
- data/lib/clacky/session_manager.rb +1 -0
- data/lib/clacky/tools/terminal.rb +30 -3
- data/lib/clacky/tools/web_search.rb +45 -6
- data/lib/clacky/ui2/components/command_suggestions.rb +17 -0
- data/lib/clacky/ui2/components/input_area.rb +26 -2
- data/lib/clacky/ui2/ui_controller.rb +17 -0
- data/lib/clacky/ui_interface.rb +9 -0
- data/lib/clacky/utils/gitignore_parser.rb +7 -1
- data/lib/clacky/utils/model_pricing.rb +0 -10
- data/lib/clacky/utils/searcher_manager.rb +87 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +530 -197
- data/lib/clacky/web/app.js +28 -3
- data/lib/clacky/web/components/chat-navigator.js +260 -0
- data/lib/clacky/web/features/billing/view.js +2 -2
- data/lib/clacky/web/features/brand/view.js +5 -1
- data/lib/clacky/web/features/extensions/store.js +214 -9
- data/lib/clacky/web/features/extensions/view.js +158 -13
- data/lib/clacky/web/features/new-session/store.js +25 -3
- data/lib/clacky/web/features/new-session/view.js +337 -140
- data/lib/clacky/web/features/skills/store.js +4 -2
- data/lib/clacky/web/features/skills/view.js +1 -1
- data/lib/clacky/web/i18n.js +72 -18
- data/lib/clacky/web/index.html +71 -64
- data/lib/clacky/web/projects.js +36 -19
- data/lib/clacky/web/sessions.js +173 -8
- data/lib/clacky/web/settings.js +85 -0
- data/lib/clacky/web/skills.js +24 -4
- data/lib/clacky/web/utils.js +1 -0
- data/lib/clacky/web/ws-dispatcher.js +63 -3
- data/lib/clacky.rb +2 -0
- metadata +7 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Creator Center - full-page workspace mounted from the sidebar.
|
|
2
2
|
// One rail entry opens a page (#ext/ext-studio) with two top tabs:
|
|
3
3
|
// • Extensions — local extension picker + verify (debug) + pack/publish
|
|
4
4
|
// • Skills — cloud/local skills, publish, iterate, create new
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
(function () {
|
|
9
9
|
const STUDIO_I18N = {
|
|
10
10
|
en: {
|
|
11
|
-
"nav.entry": "
|
|
12
|
-
"ws.title": "
|
|
11
|
+
"nav.entry": "Creator Center",
|
|
12
|
+
"ws.title": "Creator Center",
|
|
13
13
|
"tab.extensions": "Extensions",
|
|
14
14
|
"tab.skills": "Skills",
|
|
15
15
|
"ext.debug.section": "Debug",
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
"publish.changelog.placeholder": "What changed in this version?",
|
|
38
38
|
"publish.force": "Publish a new version (already published)",
|
|
39
39
|
"btn.publish": "Publish to marketplace",
|
|
40
|
+
"btn.publishBrand": "Publish to brand",
|
|
40
41
|
"btn.publishing": "Publishing…",
|
|
41
42
|
"btn.pack": "Pack (.zip)",
|
|
42
43
|
"btn.packing": "Packing…",
|
|
@@ -78,6 +79,9 @@
|
|
|
78
79
|
"pub.title.new": "Publish to the marketplace",
|
|
79
80
|
"pub.title.update": "Publish a new version",
|
|
80
81
|
"pub.intro": "\"{{name}}\" will be packed and uploaded to the extension marketplace, where anyone can discover and install it.",
|
|
82
|
+
"pub.brand.title.new": "Publish to brand",
|
|
83
|
+
"pub.brand.title.update": "Publish new version to brand",
|
|
84
|
+
"pub.brand.intro": "\"{{name}}\" will be packed and uploaded to your brand's extension library, where only your brand's users can install it.",
|
|
81
85
|
"pub.version.new": "First release · v{{ver}}",
|
|
82
86
|
"pub.version.update": "v{{prev}} → v{{ver}}",
|
|
83
87
|
"pub.version.missing": "No version in ext.yml — add a \"version\" field before publishing.",
|
|
@@ -130,6 +134,7 @@
|
|
|
130
134
|
"extlist.verify.errors": "{{n}} error(s)",
|
|
131
135
|
"extlist.verify.warnings": "{{n}} warning(s)",
|
|
132
136
|
"extlist.btn.publish": "Publish to Marketplace",
|
|
137
|
+
"extlist.btn.publishBrand": "Publish to Brand",
|
|
133
138
|
"extlist.btn.update": "Update",
|
|
134
139
|
"extlist.btn.pack": "Pack (.zip)",
|
|
135
140
|
"extlist.btn.packing": "Packing…",
|
|
@@ -146,7 +151,7 @@
|
|
|
146
151
|
"extlist.newExt.label": "Create a new extension",
|
|
147
152
|
"extlist.newExt.hint": "Opens an AI session that scaffolds and builds it for you.",
|
|
148
153
|
"extlist.newExt.btn": "Create New Extension",
|
|
149
|
-
"extlist.newExt.seed": "I want to build a new
|
|
154
|
+
"extlist.newExt.seed": "I want to build a new extension. Help me get started: ask me what the extension should do, then scaffold it and build it out step by step.",
|
|
150
155
|
"unit.panel": "panel",
|
|
151
156
|
"unit.panels": "panels",
|
|
152
157
|
"unit.agent": "agent",
|
|
@@ -193,10 +198,10 @@
|
|
|
193
198
|
"skills.publishing": "Publishing…",
|
|
194
199
|
},
|
|
195
200
|
zh: {
|
|
196
|
-
"nav.entry": "
|
|
197
|
-
"ws.title": "
|
|
201
|
+
"nav.entry": "创作者中心",
|
|
202
|
+
"ws.title": "创作者中心",
|
|
198
203
|
"tab.extensions": "扩展",
|
|
199
|
-
"tab.skills": "
|
|
204
|
+
"tab.skills": "技能",
|
|
200
205
|
"ext.debug.section": "调试",
|
|
201
206
|
"ext.publish.section": "发布",
|
|
202
207
|
"debug.tab": "调试",
|
|
@@ -222,6 +227,7 @@
|
|
|
222
227
|
"publish.changelog.placeholder": "这个版本改了什么?",
|
|
223
228
|
"publish.force": "发布新版本(已发布过)",
|
|
224
229
|
"btn.publish": "发布到市场",
|
|
230
|
+
"btn.publishBrand": "发布到品牌",
|
|
225
231
|
"btn.publishing": "发布中…",
|
|
226
232
|
"btn.pack": "打包 (.zip)",
|
|
227
233
|
"btn.packing": "打包中…",
|
|
@@ -263,6 +269,9 @@
|
|
|
263
269
|
"pub.title.new": "发布到市场",
|
|
264
270
|
"pub.title.update": "发布新版本",
|
|
265
271
|
"pub.intro": "「{{name}}」将被打包并上传到扩展市场,任何人都能发现并安装它。",
|
|
272
|
+
"pub.brand.title.new": "发布到品牌",
|
|
273
|
+
"pub.brand.title.update": "发布新版本到品牌",
|
|
274
|
+
"pub.brand.intro": "「{{name}}」将被打包并上传到你的品牌扩展库,仅品牌下的用户可安装使用。",
|
|
266
275
|
"pub.version.new": "首次发布 · v{{ver}}",
|
|
267
276
|
"pub.version.update": "v{{prev}} → v{{ver}}",
|
|
268
277
|
"pub.version.missing": "ext.yml 里没有 version — 请先补上 version 字段再发布。",
|
|
@@ -315,6 +324,7 @@
|
|
|
315
324
|
"extlist.verify.errors": "{{n}} 个错误",
|
|
316
325
|
"extlist.verify.warnings": "{{n}} 个警告",
|
|
317
326
|
"extlist.btn.publish": "发布到市场",
|
|
327
|
+
"extlist.btn.publishBrand": "发布到品牌",
|
|
318
328
|
"extlist.btn.update": "更新到市场",
|
|
319
329
|
"extlist.btn.pack": "打包(.zip)",
|
|
320
330
|
"extlist.btn.packing": "打包中…",
|
|
@@ -328,10 +338,10 @@
|
|
|
328
338
|
"extlist.unpublish.confirm": "确定要从市场下架 {{id}} 吗?",
|
|
329
339
|
"extlist.publishing": "发布中…",
|
|
330
340
|
"extlist.needLicense": "发布需要已激活的用户授权。",
|
|
331
|
-
"extlist.newExt.label": "
|
|
341
|
+
"extlist.newExt.label": "创建新扩展",
|
|
332
342
|
"extlist.newExt.hint": "打开一个 AI 会话,帮你生成并开发它。",
|
|
333
|
-
"extlist.newExt.btn": "
|
|
334
|
-
"extlist.newExt.seed": "
|
|
343
|
+
"extlist.newExt.btn": "创建新扩展",
|
|
344
|
+
"extlist.newExt.seed": "我想开发一个新的扩展。请引导我开始:先问清楚这个扩展要做什么,然后帮我搭好骨架并一步步开发出来。",
|
|
335
345
|
"unit.panel": "个面板",
|
|
336
346
|
"unit.panels": "个面板",
|
|
337
347
|
"unit.agent": "个 Agent",
|
|
@@ -351,13 +361,13 @@
|
|
|
351
361
|
"meta.saved": "已保存",
|
|
352
362
|
"meta.save.err": "保存失败:{{msg}}",
|
|
353
363
|
|
|
354
|
-
"skills.section.cloud": "
|
|
364
|
+
"skills.section.cloud": "云端技能",
|
|
355
365
|
"skills.section.cloudHint": "已发布到平台",
|
|
356
|
-
"skills.section.local": "
|
|
366
|
+
"skills.section.local": "本地技能",
|
|
357
367
|
"skills.section.localHint": "可发布",
|
|
358
|
-
"skills.cloud.empty": "
|
|
359
|
-
"skills.cloud.locked": "
|
|
360
|
-
"skills.local.empty": "
|
|
368
|
+
"skills.cloud.empty": "还没有发布任何技能。",
|
|
369
|
+
"skills.cloud.locked": "成为创作者后才能上传并发布技能。",
|
|
370
|
+
"skills.local.empty": "所有本地技能都已发布。",
|
|
361
371
|
"skills.badge.published": "已发布",
|
|
362
372
|
"skills.badge.unpublished": "未发布",
|
|
363
373
|
"skills.changed": "有本地改动",
|
|
@@ -367,14 +377,14 @@
|
|
|
367
377
|
"skills.btn.update": "更新",
|
|
368
378
|
"skills.btn.upToDate": "已是最新",
|
|
369
379
|
"skills.btn.iterate": "迭代",
|
|
370
|
-
"skills.iterate.prompt": "
|
|
380
|
+
"skills.iterate.prompt": "更新技能:",
|
|
371
381
|
"skills.shadow.label": "本地覆盖",
|
|
372
|
-
"skills.shadow.tooltip": "
|
|
373
|
-
"skills.newSkill.label": "用 /skill-creator
|
|
374
|
-
"skills.newSkill.btn": "
|
|
375
|
-
"skills.promo.text": "
|
|
382
|
+
"skills.shadow.tooltip": "本地副本覆盖了同名品牌技能",
|
|
383
|
+
"skills.newSkill.label": "用 /skill-creator 创建新技能",
|
|
384
|
+
"skills.newSkill.btn": "创建新技能",
|
|
385
|
+
"skills.promo.text": "发布你的技能,打造自己的品牌。",
|
|
376
386
|
"skills.promo.link": "了解更多 →",
|
|
377
|
-
"skills.locked": "
|
|
387
|
+
"skills.locked": "发布云端技能需要创作者授权。",
|
|
378
388
|
"skills.publishing": "发布中…",
|
|
379
389
|
},
|
|
380
390
|
};
|
|
@@ -389,6 +399,10 @@
|
|
|
389
399
|
|
|
390
400
|
function api(path) { return `/api/ext/ext-studio${path}`; }
|
|
391
401
|
|
|
402
|
+
function toast(msg, type) {
|
|
403
|
+
if (window.Clacky && Clacky.Modal && Clacky.Modal.toast) Clacky.Modal.toast(msg, type || "info");
|
|
404
|
+
}
|
|
405
|
+
|
|
392
406
|
async function getJson(path) {
|
|
393
407
|
const res = await fetch(api(path));
|
|
394
408
|
const data = await res.json();
|
|
@@ -597,8 +611,12 @@
|
|
|
597
611
|
// `prevVersionOrPromise` may be a value or a Promise.
|
|
598
612
|
// Resolves true when a publish succeeded, false otherwise.
|
|
599
613
|
|
|
600
|
-
function runPublishFlow(ext, prevVersionOrPromise, { onPublishDone } = {}) {
|
|
614
|
+
function runPublishFlow(ext, prevVersionOrPromise, { onPublishDone, origin } = {}) {
|
|
601
615
|
return new Promise((resolve) => {
|
|
616
|
+
const isBrand = origin === "self";
|
|
617
|
+
const titleNew = isBrand ? t("pub.brand.title.new") : t("pub.title.new");
|
|
618
|
+
const titleUpdate = isBrand ? t("pub.brand.title.update") : t("pub.title.update");
|
|
619
|
+
const introText = isBrand ? t("pub.brand.intro", { name: ext.name }) : t("pub.intro", { name: ext.name });
|
|
602
620
|
let currentVersion = ext.version || "";
|
|
603
621
|
let isUpdate = false;
|
|
604
622
|
|
|
@@ -628,7 +646,7 @@
|
|
|
628
646
|
status.style.display = "none";
|
|
629
647
|
|
|
630
648
|
const bodyChildren = [
|
|
631
|
-
el("p", { class: "studio-modal-intro", text:
|
|
649
|
+
el("p", { class: "studio-modal-intro", text: introText }),
|
|
632
650
|
verField,
|
|
633
651
|
notesField,
|
|
634
652
|
status,
|
|
@@ -636,7 +654,7 @@
|
|
|
636
654
|
|
|
637
655
|
let done = false;
|
|
638
656
|
const modal = openModal({
|
|
639
|
-
title:
|
|
657
|
+
title: titleNew,
|
|
640
658
|
body: el("div", null, bodyChildren),
|
|
641
659
|
buttons: [
|
|
642
660
|
{ label: t("pub.btn.cancel"), keepOpen: false, onClick: () => { if (!done) resolve(false); } },
|
|
@@ -657,7 +675,7 @@
|
|
|
657
675
|
if (done || !prevVersion) return;
|
|
658
676
|
isUpdate = true;
|
|
659
677
|
const titleEl = modal.overlay.querySelector(".studio-modal-title");
|
|
660
|
-
if (titleEl) titleEl.textContent =
|
|
678
|
+
if (titleEl) titleEl.textContent = titleUpdate;
|
|
661
679
|
if (prevVersion !== currentVersion) {
|
|
662
680
|
currentVersion = prevVersion;
|
|
663
681
|
verInput.value = currentVersion;
|
|
@@ -708,6 +726,7 @@
|
|
|
708
726
|
ext_id: ext.id,
|
|
709
727
|
force: isUpdate,
|
|
710
728
|
changelog: notes.value.trim(),
|
|
729
|
+
origin: origin || "marketplace",
|
|
711
730
|
});
|
|
712
731
|
|
|
713
732
|
if (data === null) { resetButtons(); return; }
|
|
@@ -1280,7 +1299,27 @@
|
|
|
1280
1299
|
})
|
|
1281
1300
|
.catch(() => null);
|
|
1282
1301
|
const ok = await runPublishFlow(ext, prevVersionPromise, { onPublishDone: loadPublished });
|
|
1283
|
-
if (ok)
|
|
1302
|
+
if (ok) {
|
|
1303
|
+
toast(t("pub.success"), "success");
|
|
1304
|
+
loadPublished();
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
async function doPublishBrand() {
|
|
1309
|
+
const ext = store.selected();
|
|
1310
|
+
if (!ext) return;
|
|
1311
|
+
await confirmReadme(ext, { onSaved: () => rebuild() });
|
|
1312
|
+
const prevVersionPromise = getJson("/published_brand")
|
|
1313
|
+
.then((data) => {
|
|
1314
|
+
const match = (data.extensions || []).find((e) => e.id === ext.id);
|
|
1315
|
+
return match ? match.version : null;
|
|
1316
|
+
})
|
|
1317
|
+
.catch(() => null);
|
|
1318
|
+
const ok = await runPublishFlow(ext, prevVersionPromise, { onPublishDone: loadPublished, origin: "self" });
|
|
1319
|
+
if (ok) {
|
|
1320
|
+
toast(t("pub.success"), "success");
|
|
1321
|
+
loadPublished();
|
|
1322
|
+
}
|
|
1284
1323
|
}
|
|
1285
1324
|
|
|
1286
1325
|
async function doPack() {
|
|
@@ -1368,6 +1407,9 @@
|
|
|
1368
1407
|
|
|
1369
1408
|
const bar = el("div", { class: "studio-actions" });
|
|
1370
1409
|
bar.appendChild(el("button", { class: "studio-btn studio-btn-primary studio-publish-btn", text: t("btn.publish"), onclick: doPublish }));
|
|
1410
|
+
if (typeof Brand !== "undefined" && Brand.userLicensed) {
|
|
1411
|
+
bar.appendChild(el("button", { class: "studio-btn studio-publish-brand-btn", text: t("btn.publishBrand"), onclick: doPublishBrand }));
|
|
1412
|
+
}
|
|
1371
1413
|
bar.appendChild(el("button", { class: "studio-btn studio-btn-ghost studio-pack-btn", text: t("btn.pack"), onclick: doPack }));
|
|
1372
1414
|
container.appendChild(bar);
|
|
1373
1415
|
|
|
@@ -1413,6 +1455,7 @@
|
|
|
1413
1455
|
function createExtensionsPanel() {
|
|
1414
1456
|
let container = null;
|
|
1415
1457
|
let cloud = [];
|
|
1458
|
+
let brandCloud = [];
|
|
1416
1459
|
|
|
1417
1460
|
function licensed() {
|
|
1418
1461
|
return !(typeof Brand !== "undefined" && Brand.branded && !Brand.userLicensed);
|
|
@@ -1426,6 +1469,16 @@
|
|
|
1426
1469
|
} catch (_e) {
|
|
1427
1470
|
cloud = [];
|
|
1428
1471
|
}
|
|
1472
|
+
if (typeof Brand !== "undefined" && Brand.userLicensed) {
|
|
1473
|
+
try {
|
|
1474
|
+
const data = await getJson("/published_brand");
|
|
1475
|
+
brandCloud = data.extensions || [];
|
|
1476
|
+
} catch (_e) {
|
|
1477
|
+
brandCloud = [];
|
|
1478
|
+
}
|
|
1479
|
+
} else {
|
|
1480
|
+
brandCloud = [];
|
|
1481
|
+
}
|
|
1429
1482
|
}
|
|
1430
1483
|
|
|
1431
1484
|
function createExtension(idea) {
|
|
@@ -1501,6 +1554,15 @@
|
|
|
1501
1554
|
pub.addEventListener("click", () => doPublish(ext, cloudEntry ? cloudEntry.version : null));
|
|
1502
1555
|
actions.appendChild(pub);
|
|
1503
1556
|
|
|
1557
|
+
if (typeof Brand !== "undefined" && Brand.userLicensed) {
|
|
1558
|
+
const brandEntry = brandCloud.find((c) => c.id === ext.id);
|
|
1559
|
+
const brandPub = el("button", { class: "studio-btn", text: t("extlist.btn.publishBrand") });
|
|
1560
|
+
brandPub.disabled = !!ext.error_count;
|
|
1561
|
+
brandPub.title = ext.error_count ? t("extlist.verify.errors", { n: ext.error_count }) : "";
|
|
1562
|
+
brandPub.addEventListener("click", () => doPublish(ext, brandEntry ? brandEntry.version : null, "self"));
|
|
1563
|
+
actions.appendChild(brandPub);
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1504
1566
|
actions.appendChild(el("button", { class: "studio-btn", text: t("extlist.btn.iterate"), onclick: () => createExtension(t("extlist.iterate.seed", { id: ext.id })) }));
|
|
1505
1567
|
const packBtn = el("button", { class: "studio-btn studio-btn-ghost", text: t("extlist.btn.pack") });
|
|
1506
1568
|
packBtn.addEventListener("click", () => doPack(ext, packBtn));
|
|
@@ -1546,10 +1608,10 @@
|
|
|
1546
1608
|
}
|
|
1547
1609
|
}
|
|
1548
1610
|
|
|
1549
|
-
async function doPublish(ext, prevVersion) {
|
|
1611
|
+
async function doPublish(ext, prevVersion, origin) {
|
|
1550
1612
|
await confirmReadme(ext, { onSaved: () => { reload().then(rebuild); } });
|
|
1551
1613
|
const onDone = async () => { await reload(); rebuild(); };
|
|
1552
|
-
const ok = await runPublishFlow(ext, prevVersion, { onPublishDone: onDone });
|
|
1614
|
+
const ok = await runPublishFlow(ext, prevVersion, { onPublishDone: onDone, origin });
|
|
1553
1615
|
if (ok) { await reload(); rebuild(); }
|
|
1554
1616
|
}
|
|
1555
1617
|
|
|
@@ -1567,28 +1629,29 @@
|
|
|
1567
1629
|
return box;
|
|
1568
1630
|
}
|
|
1569
1631
|
|
|
1570
|
-
function rebuild() {
|
|
1632
|
+
function rebuild(headerActions) {
|
|
1571
1633
|
if (!container) return;
|
|
1572
1634
|
container.innerHTML = "";
|
|
1573
1635
|
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1636
|
+
if (headerActions) {
|
|
1637
|
+
headerActions.innerHTML = "";
|
|
1638
|
+
headerActions.appendChild(el("button", {
|
|
1639
|
+
class: "studio-btn studio-btn-primary",
|
|
1640
|
+
text: t("extlist.newExt.btn"),
|
|
1641
|
+
onclick: () => createExtension(t("extlist.newExt.seed")),
|
|
1642
|
+
}));
|
|
1643
|
+
}
|
|
1581
1644
|
|
|
1582
1645
|
container.appendChild(section("extlist.section.cloud", "extlist.section.cloudHint", cloud, cloudCard, "extlist.cloud.empty"));
|
|
1583
1646
|
container.appendChild(section("extlist.section.local", "extlist.section.localHint", store.extensions, localCard, "extlist.local.empty"));
|
|
1584
1647
|
}
|
|
1585
1648
|
|
|
1586
1649
|
return {
|
|
1587
|
-
async attach(root) {
|
|
1650
|
+
async attach(root, headerActions) {
|
|
1588
1651
|
container = el("div", { class: "studio-panel" });
|
|
1589
1652
|
root.appendChild(container);
|
|
1590
1653
|
container.innerHTML = _skeletonHtml();
|
|
1591
|
-
try { await reload(); rebuild(); }
|
|
1654
|
+
try { await reload(); rebuild(headerActions); }
|
|
1592
1655
|
catch (e) { container.innerHTML = ""; container.appendChild(el("p", { class: "studio-empty", text: t("err.generic", { msg: e.message }) })); }
|
|
1593
1656
|
},
|
|
1594
1657
|
};
|
|
@@ -1723,24 +1786,27 @@
|
|
|
1723
1786
|
return box;
|
|
1724
1787
|
}
|
|
1725
1788
|
|
|
1726
|
-
function rebuild() {
|
|
1789
|
+
function rebuild(headerActions) {
|
|
1727
1790
|
if (!container) return;
|
|
1728
1791
|
container.innerHTML = "";
|
|
1729
1792
|
|
|
1730
|
-
if (
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1793
|
+
if (headerActions) {
|
|
1794
|
+
headerActions.innerHTML = "";
|
|
1795
|
+
const newBtn = el("button", {
|
|
1796
|
+
class: "studio-btn studio-btn-primary",
|
|
1797
|
+
text: t("skills.newSkill.btn"),
|
|
1798
|
+
onclick: () => createSkill(null),
|
|
1799
|
+
});
|
|
1800
|
+
newBtn.disabled = locked;
|
|
1801
|
+
newBtn.title = locked ? t("skills.locked") : "";
|
|
1802
|
+
headerActions.appendChild(newBtn);
|
|
1735
1803
|
}
|
|
1736
1804
|
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
newBox.appendChild(newBar);
|
|
1743
|
-
container.appendChild(newBox);
|
|
1805
|
+
if (locked) {
|
|
1806
|
+
const lockHint = el("p", { class: "studio-empty", text: t("skills.locked") });
|
|
1807
|
+
lockHint.style.marginBottom = "12px";
|
|
1808
|
+
container.appendChild(lockHint);
|
|
1809
|
+
}
|
|
1744
1810
|
|
|
1745
1811
|
const cloudEmptyKey = locked ? "skills.cloud.locked" : "skills.cloud.empty";
|
|
1746
1812
|
container.appendChild(section("skills.section.cloud", "skills.section.cloudHint", cloud, cloudCard, cloudEmptyKey));
|
|
@@ -1748,11 +1814,11 @@
|
|
|
1748
1814
|
}
|
|
1749
1815
|
|
|
1750
1816
|
return {
|
|
1751
|
-
async attach(root) {
|
|
1817
|
+
async attach(root, headerActions) {
|
|
1752
1818
|
container = el("div", { class: "studio-panel" });
|
|
1753
1819
|
root.appendChild(container);
|
|
1754
1820
|
container.innerHTML = _skeletonHtml();
|
|
1755
|
-
try { await reload(); rebuild(); }
|
|
1821
|
+
try { await reload(); rebuild(headerActions); }
|
|
1756
1822
|
catch (e) { container.innerHTML = ""; container.appendChild(el("p", { class: "studio-empty", text: t("err.generic", { msg: e.message }) })); }
|
|
1757
1823
|
},
|
|
1758
1824
|
};
|
|
@@ -1768,6 +1834,8 @@
|
|
|
1768
1834
|
|
|
1769
1835
|
const header = el("div", { class: "studio-page-head" });
|
|
1770
1836
|
header.appendChild(el("h2", { class: "studio-page-title", text: t("ws.title") }));
|
|
1837
|
+
const headerActions = el("div", { class: "studio-page-head-actions" });
|
|
1838
|
+
header.appendChild(headerActions);
|
|
1771
1839
|
page.appendChild(header);
|
|
1772
1840
|
|
|
1773
1841
|
const tabsBar = el("div", { class: "studio-tabs" });
|
|
@@ -1785,8 +1853,9 @@
|
|
|
1785
1853
|
active = id;
|
|
1786
1854
|
Array.from(tabsBar.children).forEach((b) => b.classList.toggle("studio-tab-active", b.dataset.tab === id));
|
|
1787
1855
|
body.innerHTML = "";
|
|
1856
|
+
headerActions.innerHTML = "";
|
|
1788
1857
|
const tab = tabs.find((t2) => t2.id === id);
|
|
1789
|
-
if (tab) tab.build(body);
|
|
1858
|
+
if (tab) tab.build(body, headerActions);
|
|
1790
1859
|
}
|
|
1791
1860
|
|
|
1792
1861
|
tabs.forEach((tab) => {
|
|
@@ -1799,12 +1868,12 @@
|
|
|
1799
1868
|
select("extensions");
|
|
1800
1869
|
}
|
|
1801
1870
|
|
|
1802
|
-
function buildExtensionsTab(root) {
|
|
1803
|
-
createExtensionsPanel().attach(root);
|
|
1871
|
+
function buildExtensionsTab(root, headerActions) {
|
|
1872
|
+
createExtensionsPanel().attach(root, headerActions);
|
|
1804
1873
|
}
|
|
1805
1874
|
|
|
1806
|
-
function buildSkillsTab(root) {
|
|
1807
|
-
createSkillsPanel().attach(root);
|
|
1875
|
+
function buildSkillsTab(root, headerActions) {
|
|
1876
|
+
createSkillsPanel().attach(root, headerActions);
|
|
1808
1877
|
}
|
|
1809
1878
|
|
|
1810
1879
|
function navRow(labelKey, onClick) {
|
|
@@ -1904,8 +1973,10 @@
|
|
|
1904
1973
|
.studio-published-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 8px 0; font-size: 12px; border-bottom: 1px solid var(--color-border-primary); }
|
|
1905
1974
|
.studio-published-name { display: flex; align-items: center; gap: 6px; color: var(--color-text-primary); }
|
|
1906
1975
|
.studio-page { width: 100%; }
|
|
1907
|
-
.studio-page-head { margin-bottom: 20px; }
|
|
1976
|
+
.studio-page-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px; }
|
|
1977
|
+
.studio-page-head-actions { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
|
|
1908
1978
|
.studio-page-title { margin: 0; font-size: 22px; font-weight: 600; color: var(--color-text-primary); }
|
|
1979
|
+
.studio-btn-sm { padding: 5px 12px; font-size: 12px; }
|
|
1909
1980
|
.studio-tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--color-border-primary); margin-bottom: 24px; }
|
|
1910
1981
|
.studio-tab { padding: 8px 16px; border: none; background: transparent; color: var(--color-text-tertiary); cursor: pointer; font-size: 14px; font-weight: 500; border-bottom: 2px solid transparent; margin-bottom: -1px; }
|
|
1911
1982
|
.studio-tab:hover { color: var(--color-text-primary); }
|
|
@@ -24,7 +24,7 @@ Seven `contributes:` types (use one, several, or all):
|
|
|
24
24
|
- `panels` — WebUI panels (a `view.js`, no build step, no React, no iframe)
|
|
25
25
|
- `api` — one backend file `api/handler.rb`, mounted at `/api/ext/<id>/`
|
|
26
26
|
- `skills` — a `SKILL.md` under `skills/<id>/` (prompt-only capability)
|
|
27
|
-
- `agents` — a `system_prompt.md`; can reference `panels: [id]` and `skills: [id]`
|
|
27
|
+
- `agents` — a `system_prompt.md`; can reference `panels: [id]` and `skills: [id]`; `hidden: true` hides from the new-session picker
|
|
28
28
|
- `channels` — an IM adapter
|
|
29
29
|
- `patches` — monkey-patch a real class (advanced, supply-chain risk)
|
|
30
30
|
- `hooks` — lifecycle hooks like `before_tool_use` (advanced)
|
|
@@ -92,10 +92,12 @@ exactly three capabilities:
|
|
|
92
92
|
|
|
93
93
|
```js
|
|
94
94
|
Clacky.ext.ui.mount(slot, spec, opts) // inject UI into a named slot
|
|
95
|
-
Clacky.ext.subscribe(event, handler) // observe
|
|
95
|
+
Clacky.ext.subscribe(event, handler) // observe store events + live session events (read-only)
|
|
96
96
|
Clacky.ext.api.register(name, fn) // expose a named data source; api.resolve(name)
|
|
97
97
|
```
|
|
98
98
|
|
|
99
|
+
**`subscribe(event, handler)`** - two event families: (1) host **store** events (`skills:changed`, `tasks:changed`, `profile:changed`, …) and (2) **live session events** mirrored from the WebSocket stream — conversation (`session:assistant-message`, `session:tool-call`, `session:tool-result`), status/errors (`session:error`, `session:warning`, `session:update`), lifecycle (`session:renamed`, `session:deleted`, …). Payload is `{ sessionId, ...wsFields }`. Full list in the "Core Events" section of the extend-webui docs. Handlers are read-only.
|
|
100
|
+
|
|
99
101
|
**`ui.mount(slot, spec, opts)`** — `spec` is either `(container, ctx, runtime) => …` or
|
|
100
102
|
`{ create?, render }`. The render function:
|
|
101
103
|
- gets a host-owned `container` DOM element — append into it, **or** return a Node / HTML
|
|
@@ -200,7 +202,7 @@ Response helpers: `json` / `text(str)` / `send_data(bytes, content_type:, filena
|
|
|
200
202
|
dir (`ext_dir` / `File.join(ext_dir, ...)`) — uninstall deletes the whole package, so
|
|
201
203
|
anything there is lost. Package-internal writes are only for disposable caches.
|
|
202
204
|
- Host context (white-listed): `session_manager`, `registry`, `agent_config`, `config`
|
|
203
|
-
(from ext.yml), `logger`, `ext_id`, `ext_dir`.
|
|
205
|
+
(from ext.yml), `logger`, `ext_id`, `ext_dir`, `project_manager`.
|
|
204
206
|
- Drive sessions from the backend: `create_session(prompt:, profile:, …)`,
|
|
205
207
|
`submit_task(session_id, prompt)`, `dispatch_to_session(session_id, prompt)` (runs a
|
|
206
208
|
side task on a fork and returns its reply without touching the conversation).
|
|
@@ -208,6 +210,12 @@ Response helpers: `json` / `text(str)` / `send_data(bytes, content_type:, filena
|
|
|
208
210
|
and the 200-session cleanup skips - use it for dedicated extension sessions you
|
|
209
211
|
manage yourself. Still reachable by `session_id` via `submit_task` /
|
|
210
212
|
`dispatch_to_session` / `registry.with_session`; forking resets `hidden` to false.
|
|
213
|
+
- **Projects**: `project_manager.all` lists projects, `find(id)` returns one (or
|
|
214
|
+
`nil`), `create(name:, working_dir:, …)` / `update(id, …)` / `delete(id)` mutate.
|
|
215
|
+
Pass `project_id:` to `create_session` to bind a session to a project - its
|
|
216
|
+
`working_dir` is inherited (unless overridden) and `agent.project_id` is persisted.
|
|
217
|
+
A panel can also `fetch("/api/projects")` directly (same-origin, no-auth; see
|
|
218
|
+
[Host API](/docs/extend-host-api)).
|
|
211
219
|
- Public (no-auth) endpoints: call `public_endpoint("/path")` in the class **and** set
|
|
212
220
|
`public: true` at ext.yml top level — both are required.
|
|
213
221
|
|
|
@@ -464,7 +472,7 @@ scaffold, verify, and reload is the whole job. Only publish when the user explic
|
|
|
464
472
|
to share, ship, or list the extension for others. Never publish on your own initiative or
|
|
465
473
|
as a "wrap up" of the build.
|
|
466
474
|
|
|
467
|
-
The **
|
|
475
|
+
The **Creator Center panel** has a Publish button — prefer it for a
|
|
468
476
|
guided flow. Use the CLI below for scripted/CI publishing.
|
|
469
477
|
|
|
470
478
|
### Before publishing
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
id: git
|
|
2
2
|
name: Git
|
|
3
|
+
display_name: Git
|
|
4
|
+
display_name_zh: Git
|
|
3
5
|
description: Changes panel — a friendly view of what the AI changed in the working tree
|
|
6
|
+
description_zh: 变更面板 — 直观查看 AI 在工作区的每一处修改
|
|
4
7
|
version: "0.1.0"
|
|
5
8
|
author: OpenClacky
|
|
6
9
|
homepage: https://www.openclacky.com
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
id: time_machine
|
|
2
2
|
name: Time Machine
|
|
3
|
+
display_name: Time Machine
|
|
4
|
+
display_name_zh: 时间机器
|
|
3
5
|
description: Vertical timeline of session tasks with per-task diff and one-click restore
|
|
6
|
+
description_zh: 会话任务时间轴,支持按任务查看差异并一键还原
|
|
4
7
|
version: "0.1.0"
|
|
5
8
|
author: OpenClacky
|
|
6
9
|
homepage: https://www.openclacky.com
|
|
@@ -9,3 +12,4 @@ contributes:
|
|
|
9
12
|
panels:
|
|
10
13
|
- id: time_machine
|
|
11
14
|
view: panels/time_machine/view.js
|
|
15
|
+
attach: ["*"]
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
# encoding: utf-8
|
|
4
|
+
|
|
5
|
+
#
|
|
6
|
+
# Clacky Tavily Searcher — CLI interface
|
|
7
|
+
#
|
|
8
|
+
# Usage:
|
|
9
|
+
# ruby tavily.rb "<query>" [max_results]
|
|
10
|
+
#
|
|
11
|
+
# Input:
|
|
12
|
+
# ENV["CLACKY_SEARCH_KEY"] — Tavily API key (set from ~/.clacky/search.yml)
|
|
13
|
+
#
|
|
14
|
+
# Output:
|
|
15
|
+
# stdout — JSON array of {"title","url","snippet"}
|
|
16
|
+
# stderr — error messages
|
|
17
|
+
# exit 0 — success
|
|
18
|
+
# exit 1 — failure
|
|
19
|
+
#
|
|
20
|
+
# Dependencies: none (stdlib only)
|
|
21
|
+
#
|
|
22
|
+
# This file lives in ~/.clacky/searchers/ and can be modified by the user.
|
|
23
|
+
# Copy it as a starting point for other providers (Serper, Brave, an internal
|
|
24
|
+
# search service...) — anything honouring the contract above works.
|
|
25
|
+
#
|
|
26
|
+
# VERSION: 1
|
|
27
|
+
|
|
28
|
+
require "net/http"
|
|
29
|
+
require "json"
|
|
30
|
+
require "uri"
|
|
31
|
+
|
|
32
|
+
API_URL = "https://api.tavily.com/search"
|
|
33
|
+
TIMEOUT = 20
|
|
34
|
+
|
|
35
|
+
def fail_with(message)
|
|
36
|
+
warn message
|
|
37
|
+
exit 1
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
query = ARGV[0].to_s.strip
|
|
41
|
+
max_results = (ARGV[1] || 10).to_i
|
|
42
|
+
max_results = 10 if max_results <= 0
|
|
43
|
+
|
|
44
|
+
fail_with("Usage: tavily.rb \"<query>\" [max_results]") if query.empty?
|
|
45
|
+
|
|
46
|
+
api_key = ENV["CLACKY_SEARCH_KEY"].to_s.strip
|
|
47
|
+
fail_with("Tavily API key is not configured") if api_key.empty?
|
|
48
|
+
|
|
49
|
+
uri = URI(API_URL)
|
|
50
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
51
|
+
http.use_ssl = true
|
|
52
|
+
http.open_timeout = TIMEOUT
|
|
53
|
+
http.read_timeout = TIMEOUT
|
|
54
|
+
|
|
55
|
+
request = Net::HTTP::Post.new(uri)
|
|
56
|
+
request["Content-Type"] = "application/json"
|
|
57
|
+
request["Authorization"] = "Bearer #{api_key}"
|
|
58
|
+
request.body = JSON.generate(
|
|
59
|
+
query: query,
|
|
60
|
+
search_depth: "basic",
|
|
61
|
+
max_results: [max_results, 20].min,
|
|
62
|
+
include_answer: false,
|
|
63
|
+
include_images: false,
|
|
64
|
+
include_raw_content: false
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
begin
|
|
68
|
+
response = http.request(request)
|
|
69
|
+
rescue StandardError => e
|
|
70
|
+
fail_with("Tavily request failed: #{e.message}")
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
74
|
+
detail = begin
|
|
75
|
+
JSON.parse(response.body)["detail"] || response.body
|
|
76
|
+
rescue StandardError
|
|
77
|
+
response.body
|
|
78
|
+
end
|
|
79
|
+
fail_with("Tavily returned #{response.code}: #{detail.to_s[0, 200]}")
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
begin
|
|
83
|
+
payload = JSON.parse(response.body)
|
|
84
|
+
rescue JSON::ParserError => e
|
|
85
|
+
fail_with("Tavily returned malformed JSON: #{e.message}")
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
results = Array(payload["results"]).filter_map do |item|
|
|
89
|
+
next unless item.is_a?(Hash)
|
|
90
|
+
|
|
91
|
+
url = item["url"].to_s
|
|
92
|
+
next if url.empty?
|
|
93
|
+
|
|
94
|
+
snippet = item["content"].to_s.gsub(/\s+/, " ").strip
|
|
95
|
+
{ "title" => item["title"].to_s, "url" => url, "snippet" => snippet[0, 400] }
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
puts JSON.generate(results)
|
|
@@ -17,20 +17,25 @@ When user wants to create a new Rails project:
|
|
|
17
17
|
## Process Steps
|
|
18
18
|
|
|
19
19
|
### 0. Ask Project Type and Requirement
|
|
20
|
-
Before doing anything,
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
Before doing anything, call `request_user_feedback` with a structured prompt. Use the following as a guide:
|
|
21
|
+
|
|
22
|
+
zh:
|
|
23
|
+
```json
|
|
24
|
+
{
|
|
25
|
+
"question": "请选择项目类型,并用一句话描述你想做什么:",
|
|
26
|
+
"context": "开始新项目前需要知道项目类型和需求,请先完成填写。",
|
|
27
|
+
"options": ["⚡ Demo — 无数据库,AI 自由发挥,快速原型", "🏗️ Production — 真实应用,可部署,完整 Rails 配置"]
|
|
28
|
+
}
|
|
25
29
|
```
|
|
26
30
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
en:
|
|
32
|
+
```json
|
|
33
|
+
{
|
|
34
|
+
"question": "Choose a project type and describe what you want to build in one sentence:",
|
|
35
|
+
"context": "Tell us what kind of project to create so we can get started.",
|
|
36
|
+
"options": ["⚡ Demo — no database, AI builds freely, quick prototype", "🏗️ Production — real app, ready to deploy, full Rails setup"]
|
|
37
|
+
}
|
|
38
|
+
```
|
|
34
39
|
|
|
35
40
|
**Based on user's choice:**
|
|
36
41
|
- If **Demo**: do NOT follow the Rails setup steps below. Instead, freely build a simple HTML/CSS/JS (or React) prototype directly in the working directory based on their requirement. Use your creativity.
|