openclacky 1.3.7 → 1.3.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +42 -0
  3. data/lib/clacky/agent/hook_manager.rb +10 -1
  4. data/lib/clacky/agent/system_prompt_builder.rb +1 -1
  5. data/lib/clacky/agent.rb +9 -2
  6. data/lib/clacky/agent_profile.rb +7 -4
  7. data/lib/clacky/billing/billing_store.rb +3 -2
  8. data/lib/clacky/brand_config.rb +2 -1
  9. data/lib/clacky/default_extensions/coding/agents/coding/avatar.png +0 -0
  10. data/lib/clacky/default_extensions/coding/ext.yml +4 -4
  11. data/lib/clacky/default_extensions/ext-studio/agents/ext-developer/avatar.png +0 -0
  12. data/lib/clacky/default_extensions/ext-studio/agents/ext-developer/system_prompt.md +0 -1
  13. data/lib/clacky/default_extensions/ext-studio/api/handler.rb +4 -3
  14. data/lib/clacky/default_extensions/ext-studio/ext.yml +1 -1
  15. data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +118 -50
  16. data/lib/clacky/default_extensions/ext-studio/skills/ext-publish/SKILL.md +1 -2
  17. data/lib/clacky/default_extensions/general/agents/general/avatar.png +0 -0
  18. data/lib/clacky/default_extensions/general/ext.yml +4 -4
  19. data/lib/clacky/default_extensions/git/ext.yml +0 -1
  20. data/lib/clacky/default_extensions/git/panels/git/view.js +3 -3
  21. data/lib/clacky/default_extensions/meeting/ext.yml +0 -1
  22. data/lib/clacky/default_extensions/meeting/panels/meeting/view.js +1 -2
  23. data/lib/clacky/default_extensions/time_machine/ext.yml +0 -1
  24. data/lib/clacky/extension/api_extension.rb +11 -2
  25. data/lib/clacky/extension/dispatcher.rb +3 -2
  26. data/lib/clacky/extension/loader.rb +6 -0
  27. data/lib/clacky/extension/packager.rb +0 -6
  28. data/lib/clacky/extension/verifier.rb +1 -1
  29. data/lib/clacky/server/http_server.rb +149 -5
  30. data/lib/clacky/shell_hook_loader.rb +266 -22
  31. data/lib/clacky/ui2/components/welcome_banner.rb +1 -1
  32. data/lib/clacky/utils/workspace_rules.rb +2 -2
  33. data/lib/clacky/version.rb +1 -1
  34. data/lib/clacky/web/app.css +334 -73
  35. data/lib/clacky/web/core/ext.js +130 -24
  36. data/lib/clacky/web/features/extensions/store.js +41 -6
  37. data/lib/clacky/web/features/extensions/view.js +44 -13
  38. data/lib/clacky/web/features/new-session/store.js +13 -0
  39. data/lib/clacky/web/features/new-session/view.js +314 -14
  40. data/lib/clacky/web/features/trash/view.js +4 -5
  41. data/lib/clacky/web/i18n.js +44 -2
  42. data/lib/clacky/web/index.html +54 -16
  43. data/lib/clacky/web/sessions.js +3 -3
  44. data/lib/clacky/web/skills.js +86 -48
  45. data/lib/clacky/web/theme.js +3 -0
  46. data/lib/clacky/web/ws-dispatcher.js +11 -2
  47. metadata +7 -7
@@ -29,7 +29,7 @@
29
29
  "verify.errors": "{{n}} error(s)",
30
30
  "verify.warnings": "{{n}} warning(s)",
31
31
  "verify.hint": "Hint",
32
- "hint.reload": "After fixing, reload this page extensions hot-reload per request.",
32
+ "hint.reload": "After fixing, reload this page to apply changes.",
33
33
  "publish.status": "Status",
34
34
  "publish.status.draft": "Draft",
35
35
  "publish.status.published": "Published",
@@ -39,9 +39,9 @@
39
39
  "btn.publish": "Publish to marketplace",
40
40
  "btn.publishing": "Publishing…",
41
41
  "btn.pack": "Pack (.zip)",
42
+ "btn.packing": "Packing…",
42
43
  "publish.needLicense": "Publishing requires an activated user license.",
43
44
  "publish.done": "Published {{id}} {{ver}} — {{status}}",
44
- "publish.packed": "Packed → {{path}}",
45
45
  "publish.already": "Already published. Enable \"publish a new version\" to ship.",
46
46
  "published.title": "Your published extensions",
47
47
  "published.empty": "You haven't published anything yet.",
@@ -98,6 +98,7 @@
98
98
  "extlist.btn.publish": "Publish",
99
99
  "extlist.btn.update": "Update",
100
100
  "extlist.btn.pack": "Pack",
101
+ "extlist.btn.packing": "Packing…",
101
102
  "extlist.btn.unpublish": "Unpublish",
102
103
  "extlist.btn.iterate": "Iterate",
103
104
  "extlist.iterate.seed": "Iterate on extension {{id}}",
@@ -105,7 +106,6 @@
105
106
  "extlist.overwrite.confirm": "\"{{id}}\" is already published. Publish a new version?",
106
107
  "extlist.unpublish.confirm": "Unpublish {{id}} from the marketplace?",
107
108
  "extlist.publishing": "Publishing…",
108
- "extlist.packed": "Packed → {{path}}",
109
109
  "extlist.needLicense": "Publishing requires an activated user license.",
110
110
  "extlist.newExt.label": "Create a new extension",
111
111
  "extlist.newExt.hint": "Opens an AI session that scaffolds and builds it for you.",
@@ -159,7 +159,7 @@
159
159
  "verify.errors": "{{n}} 个错误",
160
160
  "verify.warnings": "{{n}} 个警告",
161
161
  "verify.hint": "提示",
162
- "hint.reload": "修复后刷新本页 —— 扩展每次请求热重载。",
162
+ "hint.reload": "修复问题后,刷新页面即可生效。",
163
163
  "publish.status": "状态",
164
164
  "publish.status.draft": "草稿",
165
165
  "publish.status.published": "已发布",
@@ -169,9 +169,9 @@
169
169
  "btn.publish": "发布到市场",
170
170
  "btn.publishing": "发布中…",
171
171
  "btn.pack": "打包 (.zip)",
172
+ "btn.packing": "打包中…",
172
173
  "publish.needLicense": "发布需要已激活的用户授权。",
173
174
  "publish.done": "已发布 {{id}} {{ver}} — {{status}}",
174
- "publish.packed": "已打包 → {{path}}",
175
175
  "publish.already": "已发布过。勾选「发布新版本」后再试。",
176
176
  "published.title": "你发布的扩展",
177
177
  "published.empty": "你还没有发布任何扩展。",
@@ -228,6 +228,7 @@
228
228
  "extlist.btn.publish": "发布",
229
229
  "extlist.btn.update": "更新",
230
230
  "extlist.btn.pack": "打包",
231
+ "extlist.btn.packing": "打包中…",
231
232
  "extlist.btn.unpublish": "下架",
232
233
  "extlist.btn.iterate": "迭代",
233
234
  "extlist.iterate.seed": "迭代扩展 {{id}}",
@@ -235,7 +236,6 @@
235
236
  "extlist.overwrite.confirm": "「{{id}}」已经发布过了。要发布新版本吗?",
236
237
  "extlist.unpublish.confirm": "确定要从市场下架 {{id}} 吗?",
237
238
  "extlist.publishing": "发布中…",
238
- "extlist.packed": "已打包 → {{path}}",
239
239
  "extlist.needLicense": "发布需要已激活的用户授权。",
240
240
  "extlist.newExt.label": "新建扩展",
241
241
  "extlist.newExt.hint": "打开一个 AI 会话,帮你生成并开发它。",
@@ -302,6 +302,28 @@
302
302
  return data;
303
303
  }
304
304
 
305
+ async function downloadPack(ext_id) {
306
+ const res = await fetch(api("/pack"), {
307
+ method: "POST",
308
+ headers: { "Content-Type": "application/json" },
309
+ body: JSON.stringify({ ext_id }),
310
+ });
311
+ if (!res.ok) {
312
+ let msg = `Request failed (${res.status})`;
313
+ try { msg = (await res.json()).error || msg; } catch (_e) {}
314
+ throw new Error(msg);
315
+ }
316
+ const blob = await res.blob();
317
+ const url = URL.createObjectURL(blob);
318
+ const a = document.createElement("a");
319
+ a.href = url;
320
+ a.download = `${ext_id}.zip`;
321
+ document.body.appendChild(a);
322
+ a.click();
323
+ document.body.removeChild(a);
324
+ URL.revokeObjectURL(url);
325
+ }
326
+
305
327
  // Skills tab talks to host-owned endpoints (not the ext prefix). We surface
306
328
  // the HTTP status so the caller can treat 403 as "locked" rather than error.
307
329
  async function getHost(path) {
@@ -452,29 +474,26 @@
452
474
  const version = ext.version;
453
475
  const isUpdate = !!prevVersion;
454
476
 
455
- const intro = el("p", { class: "studio-modal-status", text: t("pub.intro", { name: ext.name }) });
477
+ const intro = el("p", { class: "studio-modal-intro", text: t("pub.intro", { name: ext.name }) });
456
478
 
457
479
  const verText = !version
458
480
  ? t("pub.version.missing")
459
481
  : isUpdate
460
482
  ? t("pub.version.update", { prev: prevVersion, ver: version })
461
483
  : t("pub.version.new", { ver: version });
462
- const verLine = el("p", { class: "studio-modal-code", text: verText });
463
484
 
464
- const unitList = (ext.units || []).map((u) => u.kind).join(" · ");
465
- const unitsLine = unitList
466
- ? el("p", { class: "studio-skill-hint", text: t("pub.units", { units: unitList }) })
467
- : null;
485
+ const metaBlock = el("div", { class: "studio-pub-meta" });
486
+ metaBlock.appendChild(el("span", { class: "studio-pub-version", text: verText }));
468
487
 
469
- const notesLabel = el("label", { class: "studio-label", text: t("pub.notes.label") });
488
+ const notesField = el("div", { class: "studio-field" });
489
+ notesField.appendChild(el("label", { class: "studio-label", text: t("pub.notes.label") }));
470
490
  const notes = el("textarea", { class: "studio-textarea", rows: "3", placeholder: t("pub.notes.placeholder") });
491
+ notesField.appendChild(notes);
471
492
 
472
493
  const status = el("p", { class: "studio-modal-status" });
473
494
  status.style.display = "none";
474
495
 
475
- const bodyChildren = [intro, verLine];
476
- if (unitsLine) bodyChildren.push(unitsLine);
477
- bodyChildren.push(notesLabel, notes, status);
496
+ const bodyChildren = [intro, metaBlock, notesField, status];
478
497
 
479
498
  let done = false;
480
499
  const modal = openModal({
@@ -586,6 +605,7 @@
586
605
  function createDebugPanel() {
587
606
  let container = null;
588
607
  let unsub = null;
608
+ let summaryFadeTimer = null;
589
609
 
590
610
  async function runVerify() {
591
611
  const ext = store.selected();
@@ -597,6 +617,8 @@
597
617
  renderVerify(data);
598
618
  } catch (e) {
599
619
  renderError(e);
620
+ } finally {
621
+ if (status) status.textContent = t("btn.recheck");
600
622
  }
601
623
  }
602
624
 
@@ -607,16 +629,22 @@
607
629
  const errs = (data.issues || []).filter((i) => i.level === "error");
608
630
  const warns = (data.issues || []).filter((i) => i.level === "warning");
609
631
 
610
- const summary = el("div", { class: data.ok ? "studio-verify-ok" : "studio-verify-fail" });
611
- if (data.ok && !warns.length) {
612
- summary.textContent = "✓ " + t("verify.ok");
613
- } else {
614
- const parts = [];
615
- if (errs.length) parts.push(t("verify.errors", { n: errs.length }));
616
- if (warns.length) parts.push(t("verify.warnings", { n: warns.length }));
617
- summary.textContent = parts.join(" · ");
632
+ const summary = container.querySelector(".studio-verify-summary");
633
+ if (summary) {
634
+ clearTimeout(summaryFadeTimer);
635
+ summary.textContent = "";
636
+ if (data.ok && !warns.length) {
637
+ summary.textContent = "✓ " + t("verify.ok");
638
+ summary.className = "studio-verify-summary studio-verify-ok";
639
+ summaryFadeTimer = setTimeout(() => { summary.textContent = ""; }, 3000);
640
+ } else {
641
+ const parts = [];
642
+ if (errs.length) parts.push(t("verify.errors", { n: errs.length }));
643
+ if (warns.length) parts.push(t("verify.warnings", { n: warns.length }));
644
+ summary.textContent = parts.join(" · ");
645
+ summary.className = "studio-verify-summary studio-verify-fail";
646
+ }
618
647
  }
619
- box.appendChild(summary);
620
648
 
621
649
  (data.issues || []).forEach((i) => {
622
650
  const item = el("div", { class: "studio-issue studio-issue-" + i.level });
@@ -630,7 +658,9 @@
630
658
 
631
659
  function renderError(e) {
632
660
  const box = container.querySelector(".studio-verify");
633
- if (box) { box.innerHTML = ""; box.appendChild(el("div", { class: "studio-verify-fail", text: t("err.generic", { msg: e.message }) })); }
661
+ const summary = container.querySelector(".studio-verify-summary");
662
+ if (summary) { summary.textContent = t("err.generic", { msg: e.message }); summary.className = "studio-verify-summary studio-verify-fail"; }
663
+ if (box) { box.innerHTML = ""; }
634
664
  }
635
665
 
636
666
  function renderDetail() {
@@ -647,7 +677,6 @@
647
677
  const row = (label, value) => { meta.appendChild(el("dt", { text: label })); meta.appendChild(el("dd", { text: value || "—" })); };
648
678
  row(t("detail.version"), ext.version);
649
679
  row(t("detail.layer"), ext.layer);
650
- row(t("detail.origin"), ext.origin);
651
680
  detail.appendChild(meta);
652
681
 
653
682
  detail.appendChild(el("div", { class: "studio-label", text: t("detail.units") }));
@@ -674,6 +703,7 @@
674
703
 
675
704
  const bar = el("div", { class: "studio-actions" });
676
705
  bar.appendChild(el("button", { class: "studio-btn studio-btn-primary studio-verify-status", text: t("btn.recheck"), onclick: runVerify }));
706
+ bar.appendChild(el("span", { class: "studio-verify-summary" }));
677
707
  container.appendChild(bar);
678
708
 
679
709
  container.appendChild(el("div", { class: "studio-verify" }));
@@ -722,11 +752,15 @@
722
752
  async function doPack() {
723
753
  const ext = store.selected();
724
754
  if (!ext) return;
755
+ const btn = container.querySelector(".studio-pack-btn");
756
+ const orig = btn ? btn.textContent : null;
757
+ if (btn) { btn.disabled = true; btn.textContent = t("btn.packing"); }
725
758
  try {
726
- const data = await postJson("/pack", { ext_id: ext.id });
727
- feedback(t("publish.packed", { path: data.path }), "success");
759
+ await downloadPack(ext.id);
728
760
  } catch (e) {
729
761
  feedback(t("err.generic", { msg: e.message }), "error");
762
+ } finally {
763
+ if (btn) { btn.disabled = false; btn.textContent = orig; }
730
764
  }
731
765
  }
732
766
 
@@ -766,7 +800,7 @@
766
800
 
767
801
  const bar = el("div", { class: "studio-actions" });
768
802
  bar.appendChild(el("button", { class: "studio-btn studio-btn-primary studio-publish-btn", text: t("btn.publish"), onclick: doPublish }));
769
- bar.appendChild(el("button", { class: "studio-btn studio-btn-ghost", text: t("btn.pack"), onclick: doPack }));
803
+ bar.appendChild(el("button", { class: "studio-btn studio-btn-ghost studio-pack-btn", text: t("btn.pack"), onclick: doPack }));
770
804
  container.appendChild(bar);
771
805
 
772
806
  container.appendChild(el("div", { class: "studio-feedback" }));
@@ -787,6 +821,23 @@
787
821
  };
788
822
  }
789
823
 
824
+ function _skeletonHtml() {
825
+ return Array.from({ length: 3 }).map(() => `
826
+ <div class="studio-skill-card" style="pointer-events:none">
827
+ <div class="studio-skill-head">
828
+ <span class="skel" style="display:inline-block;height:1rem;width:40%"></span>
829
+ <span class="skel" style="display:inline-block;height:1.2rem;width:3.5rem;border-radius:4px;"></span>
830
+ </div>
831
+ <div class="studio-skill-meta" style="margin-top:6px">
832
+ <span class="skel" style="display:inline-block;height:0.85rem;width:20%"></span>
833
+ </div>
834
+ <div class="studio-actions">
835
+ <span class="skel" style="display:inline-block;height:1.75rem;width:5rem;border-radius:6px;"></span>
836
+ <span class="skel" style="display:inline-block;height:1.75rem;width:4rem;border-radius:6px;"></span>
837
+ </div>
838
+ </div>`).join("");
839
+ }
840
+
790
841
  // ── Extensions tab (full-page): cloud + local extension cards ──────────────
791
842
  // Reuses backend endpoints GET /published (cloud), the shared `store` (local,
792
843
  // GET /extensions), POST /publish|/pack|/unpublish, and POST /develop to open
@@ -873,17 +924,21 @@
873
924
  actions.appendChild(pub);
874
925
 
875
926
  actions.appendChild(el("button", { class: "studio-btn", text: t("extlist.btn.iterate"), onclick: () => createExtension(t("extlist.iterate.seed", { id: ext.id })) }));
876
- actions.appendChild(el("button", { class: "studio-btn studio-btn-ghost", text: t("extlist.btn.pack"), onclick: () => doPack(ext) }));
877
- card.appendChild(actions);
927
+ const packBtn = el("button", { class: "studio-btn studio-btn-ghost", text: t("extlist.btn.pack") });
928
+ packBtn.addEventListener("click", () => doPack(ext, packBtn));
929
+ actions.appendChild(packBtn);
878
930
  return card;
879
931
  }
880
932
 
881
- async function doPack(ext) {
933
+ async function doPack(ext, btn) {
934
+ const orig = btn ? btn.textContent : null;
935
+ if (btn) { btn.disabled = true; btn.textContent = t("extlist.btn.packing"); }
882
936
  try {
883
- const data = await postJson("/pack", { ext_id: ext.id });
884
- alert(t("extlist.packed", { path: data.path }));
937
+ await downloadPack(ext.id);
885
938
  } catch (e) {
886
939
  alert(t("err.generic", { msg: e.message }));
940
+ } finally {
941
+ if (btn) { btn.disabled = false; btn.textContent = orig; }
887
942
  }
888
943
  }
889
944
 
@@ -926,7 +981,7 @@
926
981
  async attach(root) {
927
982
  container = el("div", { class: "studio-panel" });
928
983
  root.appendChild(container);
929
- container.appendChild(el("p", { class: "studio-hint", text: t("extlist.loading") }));
984
+ container.innerHTML = _skeletonHtml();
930
985
  try { await reload(); rebuild(); }
931
986
  catch (e) { container.innerHTML = ""; container.appendChild(el("p", { class: "studio-empty", text: t("err.generic", { msg: e.message }) })); }
932
987
  },
@@ -1090,7 +1145,7 @@
1090
1145
  async attach(root) {
1091
1146
  container = el("div", { class: "studio-panel" });
1092
1147
  root.appendChild(container);
1093
- container.appendChild(el("p", { class: "studio-hint", text: t("skills.loading") }));
1148
+ container.innerHTML = _skeletonHtml();
1094
1149
  try { await reload(); rebuild(); }
1095
1150
  catch (e) { container.innerHTML = ""; container.appendChild(el("p", { class: "studio-empty", text: t("err.generic", { msg: e.message }) })); }
1096
1151
  },
@@ -1146,7 +1201,7 @@
1146
1201
  createSkillsPanel().attach(root);
1147
1202
  }
1148
1203
 
1149
- function navRow(label, onClick) {
1204
+ function navRow(labelKey, onClick) {
1150
1205
  const item = el("div", { class: "task-item task-item-summary" });
1151
1206
  item.innerHTML =
1152
1207
  '<div class="task-row">' +
@@ -1159,7 +1214,9 @@
1159
1214
  '</svg>' +
1160
1215
  '<div class="task-info"><span class="task-name"></span></div>' +
1161
1216
  '</div>';
1162
- item.querySelector(".task-name").textContent = label;
1217
+ const nameEl = item.querySelector(".task-name");
1218
+ nameEl.textContent = t(labelKey);
1219
+ document.addEventListener("langchange", () => { nameEl.textContent = t(labelKey); });
1163
1220
  item.addEventListener("click", onClick);
1164
1221
  return item;
1165
1222
  }
@@ -1170,7 +1227,7 @@
1170
1227
  });
1171
1228
 
1172
1229
  Clacky.ext.ui.mount("sidebar.nav.bottom", function () {
1173
- return navRow(t("nav.entry"), function () { Clacky.ext.ui.openWorkspace(WS_ID); });
1230
+ return navRow("nav.entry", function () { Clacky.ext.ui.openWorkspace(WS_ID); });
1174
1231
  }, { workspace: WS_ID });
1175
1232
 
1176
1233
  // Extension-developer session tools: debug + publish, as aside tabs. These
@@ -1191,6 +1248,8 @@
1191
1248
  .studio-page .studio-panel { padding: 0; }
1192
1249
  .studio-field { margin-bottom: 14px; }
1193
1250
  .studio-label { display: block; font-size: 12px; font-weight: 600; color: var(--color-text-tertiary); margin-bottom: 6px; }
1251
+ .studio-field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 16px; }
1252
+ .studio-label { font-size: 12px; font-weight: 500; color: var(--color-text-secondary); }
1194
1253
  .studio-select, .studio-textarea { width: 100%; box-sizing: border-box; background: var(--color-bg-input); border: 1px solid var(--color-border-primary); border-radius: var(--radius-sm); padding: 7px 8px; color: var(--color-text-primary); font-size: 13px; font-family: inherit; }
1195
1254
  .studio-select:focus, .studio-textarea:focus { border-color: var(--color-accent-primary); outline: none; }
1196
1255
  .studio-empty { color: var(--color-text-muted); font-size: 12px; margin: 4px 0; }
@@ -1203,7 +1262,7 @@
1203
1262
  .studio-meta dd { margin: 0; color: var(--color-text-primary); }
1204
1263
  .studio-units { display: flex; flex-wrap: wrap; gap: 6px; }
1205
1264
  .studio-unit-chip { display: inline-flex; background: var(--color-bg-hover); color: var(--color-text-primary); border: 1px solid var(--color-border-primary); border-radius: var(--radius-sm); padding: 2px 8px; font-size: 12px; }
1206
- .studio-actions { display: flex; gap: 8px; margin: 12px 0; }
1265
+ .studio-actions { display: flex; align-items: center; gap: 8px; margin: 12px 0; }
1207
1266
  .studio-skill-promo .studio-actions { margin: 12px 0 0; }
1208
1267
  .studio-btn { padding: 7px 14px; border-radius: var(--radius-sm); border: 1px solid var(--color-border-primary); background: transparent; color: var(--color-text-secondary); cursor: pointer; font-size: 13px; font-weight: 500; }
1209
1268
  .studio-btn:hover { background: var(--color-bg-hover); color: var(--color-text-primary); }
@@ -1212,11 +1271,13 @@
1212
1271
  .studio-btn-primary:disabled { opacity: 0.6; cursor: default; }
1213
1272
  .studio-btn-danger { color: var(--color-error); border-color: var(--color-error-border); padding: 4px 10px; }
1214
1273
  .studio-btn-danger:hover { background: var(--color-error-bg); color: var(--color-error); }
1215
- .studio-btn-ghost { border-color: transparent; color: var(--color-text-tertiary); font-weight: 400; }
1216
- .studio-btn-ghost:hover { background: var(--color-bg-hover); color: var(--color-text-secondary); }
1274
+ .studio-btn-ghost { color: var(--color-text-secondary); font-weight: 500; }
1275
+ .studio-btn-ghost:hover { background: var(--color-bg-hover); color: var(--color-text-primary); }
1217
1276
  .studio-verify { margin-top: 8px; }
1218
- .studio-verify-ok { color: var(--color-success); font-size: 12px; font-weight: 600; margin-bottom: 8px; }
1219
- .studio-verify-fail { color: var(--color-error); font-size: 12px; font-weight: 600; margin-bottom: 8px; }
1277
+ .studio-verify-summary { font-size: 13px; font-weight: 600; }
1278
+ .studio-verify-ok { color: var(--color-success); font-size: 12px; font-weight: 600; }
1279
+ .studio-verify-fail { color: var(--color-error); font-size: 12px; font-weight: 600; }
1280
+ .studio-verify > *:first-child { margin-bottom: 8px; }
1220
1281
  .studio-issue { border-left: 3px solid var(--color-border-primary); padding: 6px 10px; margin-bottom: 8px; background: var(--color-bg-secondary); border-radius: 0 var(--radius-sm) var(--radius-sm) 0; }
1221
1282
  .studio-issue-error { border-left-color: var(--color-error); }
1222
1283
  .studio-issue-warning { border-left-color: var(--color-warning, var(--color-text-tertiary)); }
@@ -1225,7 +1286,7 @@
1225
1286
  .studio-issue-file { font-size: 11px; font-family: monospace; color: var(--color-text-muted); margin-top: 2px; }
1226
1287
  .studio-issue-hint { font-size: 11px; color: var(--color-text-secondary); margin-top: 4px; font-style: italic; }
1227
1288
  .studio-check { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--color-text-secondary); margin-bottom: 12px; cursor: pointer; }
1228
- .studio-feedback { font-size: 12px; margin: 8px 0; min-height: 14px; line-height: 1.4; }
1289
+ .studio-feedback { font-size: 12px; margin: 6px 0 0; line-height: 1.4; }
1229
1290
  .studio-feedback-success { color: var(--color-success); }
1230
1291
  .studio-feedback-error { color: var(--color-error); }
1231
1292
  .studio-feedback-warn { color: var(--color-warning, var(--color-text-secondary)); }
@@ -1256,14 +1317,21 @@
1256
1317
  .studio-skill-meta { display: flex; gap: 12px; font-size: 11px; color: var(--color-text-muted); margin-bottom: 8px; }
1257
1318
  .studio-skill-promo { border: 1px solid var(--color-border-primary); border-radius: var(--radius-md, 8px); padding: 16px 18px; margin: 0 0 24px; background: var(--color-bg-secondary); }
1258
1319
  .studio-skill-promo-text { margin: 0 0 4px; font-size: 14px; font-weight: 600; color: var(--color-text-primary); }
1259
- .studio-modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 9999; }
1260
- .studio-modal { background: var(--color-bg-primary, var(--color-bg-secondary)); border: 1px solid var(--color-border-primary); border-radius: var(--radius-md, 8px); padding: 20px 22px; width: 420px; max-width: calc(100vw - 40px); box-shadow: 0 12px 40px rgba(0,0,0,0.35); }
1261
- .studio-modal-title { margin: 0 0 10px; font-size: 16px; font-weight: 600; color: var(--color-text-primary); }
1320
+ .studio-modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 9999; animation: studio-overlay-in 0.15s ease; }
1321
+ .studio-modal { background: var(--color-bg-primary, var(--color-bg-secondary)); border: 1px solid var(--color-border-primary); border-radius: var(--radius-md, 8px); padding: 28px 28px 22px; width: 520px; max-width: calc(100vw - 40px); box-shadow: 0 12px 40px rgba(0,0,0,0.35); animation: studio-modal-in 0.2s cubic-bezier(0.34,1.4,0.64,1); }
1322
+ @keyframes studio-overlay-in { from { opacity: 0; } to { opacity: 1; } }
1323
+ @keyframes studio-modal-in { from { opacity: 0; transform: scale(0.93) translateY(6px); } to { opacity: 1; transform: scale(1) translateY(0); } }
1324
+ .studio-modal-title { margin: 0 0 16px; font-size: 16px; font-weight: 600; color: var(--color-text-primary); }
1325
+ .studio-modal-intro { margin: 0 0 16px; font-size: 13px; color: var(--color-text-secondary); line-height: 1.6; }
1326
+ .studio-pub-meta { display: flex; align-items: center; gap: 10px; background: var(--color-bg-secondary); border: 1px solid var(--color-border-primary); border-radius: var(--radius-sm, 6px); padding: 10px 14px; margin-bottom: 16px; }
1327
+ .studio-pub-version { font-size: 13px; font-weight: 600; color: var(--color-text-primary); font-family: monospace; }
1328
+ .studio-pub-units { font-size: 12px; color: var(--color-text-tertiary); }
1329
+ .studio-pub-units::before { content: "·"; margin-right: 10px; }
1262
1330
  .studio-modal-body { font-size: 13px; color: var(--color-text-secondary); line-height: 1.6; }
1263
1331
  .studio-modal-status { margin: 0 0 8px; font-size: 13px; color: var(--color-text-secondary); line-height: 1.6; }
1264
1332
  .studio-modal-code { margin: 0 0 8px; font-size: 13px; font-family: monospace; color: var(--color-text-primary); }
1265
1333
  .studio-modal-link { display: inline-block; font-size: 13px; color: var(--color-accent-primary); }
1266
- .studio-modal-footer { display: flex; justify-content: flex-end; gap: 8px; margin-top: 18px; }
1334
+ .studio-modal-footer { display: flex; justify-content: flex-end; gap: 8px; margin-top: 20px; padding-top: 16px; border-top: 1px solid var(--color-border-primary); }
1267
1335
  `;
1268
1336
  document.head.appendChild(style);
1269
1337
  })();
@@ -11,8 +11,7 @@ it's live.
11
11
  ## Before publishing
12
12
 
13
13
  - The extension must live in the **local** layer (`~/.clacky/ext/local/<id>/`). Only
14
- local containers can be packed. Marketplace-origin and encrypted (`SKILL.md.enc`)
15
- containers are rejected.
14
+ local containers can be packed. Encrypted (`SKILL.md.enc`) containers are rejected.
16
15
  - Publishing requires an **activated user license** — it proves creator identity, and
17
16
  the platform attributes the extension to that account. If activation is missing, tell
18
17
  the user to activate first; don't try to work around it.
@@ -5,14 +5,14 @@ version: "0.1.0"
5
5
  author: OpenClacky
6
6
  homepage: https://www.openclacky.com
7
7
  license: MIT
8
- origin: self
9
8
  contributes:
10
9
  agents:
11
10
  - id: general
12
11
  title: General
13
- title_zh: 通用
14
- description: A versatile digital employee living on your computer
15
- description_zh: 驻留在你电脑里的多面手数字员工
12
+ title_zh: 日常工作
13
+ description: Handles your everyday work
14
+ description_zh: 帮你打理日常工作,查资料、写东西、整理归纳都行
16
15
  order: 1
17
16
  prompt: agents/general/system_prompt.md
17
+ avatar: agents/general/avatar.png
18
18
  panels: [meeting/meeting]
@@ -5,7 +5,6 @@ version: "0.1.0"
5
5
  author: OpenClacky
6
6
  homepage: https://www.openclacky.com
7
7
  license: MIT
8
- origin: self
9
8
  contributes:
10
9
  panels:
11
10
  - id: git
@@ -187,12 +187,12 @@
187
187
  );
188
188
  }
189
189
 
190
- Clacky.ext.ui.mount("session.aside", (ctx) => {
191
- if (!ctx || !ctx.sessionId) return null;
190
+ Clacky.ext.ui.mount("session.aside", (container, ctx) => {
191
+ if (!ctx || !ctx.sessionId) return;
192
192
  const body = el("div", { class: "changes-panel" });
193
193
  const root = el("div", { class: "changes-root", "data-panel": "changes" }, body);
194
+ container.appendChild(root);
194
195
  refresh(ctx.sessionId, root, body, ctx);
195
- return root;
196
196
  }, {
197
197
  order: 10,
198
198
  tab: { id: "changes", label: () => t("changes.tab") },
@@ -5,7 +5,6 @@ version: "0.1.0"
5
5
  author: OpenClacky
6
6
  homepage: https://www.openclacky.com
7
7
  license: MIT
8
- origin: self
9
8
  contributes:
10
9
  api: api/handler.rb
11
10
  panels:
@@ -893,7 +893,6 @@
893
893
  // Every tab activation gets a fresh empty container; wire it into state
894
894
  // and paint the current state onto it.
895
895
  attach(container) {
896
- container.className = "meeting-container";
897
896
  state.container = container;
898
897
  renderUI(container);
899
898
  },
@@ -923,7 +922,7 @@
923
922
  // Inject minimal styles
924
923
  const style = document.createElement("style");
925
924
  style.textContent = `
926
- .meeting-container { padding: 16px; font-size: 13px; color: var(--color-text-secondary); }
925
+ .meeting-panel { padding: 16px; font-size: 13px; color: var(--color-text-secondary); }
927
926
  .meeting-btn { padding: 7px 14px; border-radius: var(--radius-sm); border: 1px solid transparent; cursor: pointer; font-size: 13px; font-weight: 500; transition: background .15s, border-color .15s; }
928
927
  .meeting-btn-start { background: var(--color-button-primary); color: var(--color-button-primary-text); }
929
928
  .meeting-btn-start:hover { background: var(--color-button-primary-hover); }
@@ -5,7 +5,6 @@ version: "0.1.0"
5
5
  author: OpenClacky
6
6
  homepage: https://www.openclacky.com
7
7
  license: MIT
8
- origin: self
9
8
  contributes:
10
9
  panels:
11
10
  - id: time_machine
@@ -30,13 +30,14 @@ module Clacky
30
30
  Route = Struct.new(:method, :pattern, :regex, :param_names, :block, :options, keyword_init: true)
31
31
 
32
32
  class Halt < StandardError
33
- attr_reader :status, :payload, :content_type
33
+ attr_reader :status, :payload, :content_type, :extra_headers
34
34
 
35
- def initialize(status, payload, content_type)
35
+ def initialize(status, payload, content_type, extra_headers: {})
36
36
  super("api_ext halt #{status}")
37
37
  @status = status
38
38
  @payload = payload
39
39
  @content_type = content_type
40
+ @extra_headers = extra_headers
40
41
  end
41
42
  end
42
43
 
@@ -201,6 +202,14 @@ module Clacky
201
202
  raise Halt.new(status, str.to_s, "text/plain; charset=utf-8")
202
203
  end
203
204
 
205
+ def send_data(bytes, content_type:, filename: nil, status: 200)
206
+ disposition = filename ? "attachment; filename=\"#{filename}\"" : "attachment"
207
+ raise Halt.new(status, bytes, content_type, extra_headers: {
208
+ "Content-Disposition" => disposition,
209
+ "Content-Length" => bytes.bytesize.to_s
210
+ })
211
+ end
212
+
204
213
  def error!(message, status: 400, **extra)
205
214
  payload = { error: message.to_s }
206
215
  payload.merge!(extra) unless extra.empty?
@@ -97,7 +97,7 @@ module Clacky
97
97
  # Handler exited without writing — empty 204
98
98
  empty_response(res)
99
99
  rescue Clacky::ApiExtension::Halt => halt
100
- write_response(res, halt.status, halt.payload, halt.content_type)
100
+ write_response(res, halt.status, halt.payload, halt.content_type, halt.extra_headers)
101
101
  rescue Timeout::Error
102
102
  Clacky::Logger.warn("[api_ext:#{klass.ext_id}] Timed out after #{timeout_sec}s on #{route.method.upcase} #{route.pattern}")
103
103
  write_json(res, 503, error: "request timed out")
@@ -106,10 +106,11 @@ module Clacky
106
106
  write_json(res, 500, error: e.message)
107
107
  end
108
108
 
109
- private def write_response(res, status, body, content_type)
109
+ private def write_response(res, status, body, content_type, extra_headers = {})
110
110
  res.status = status
111
111
  res.content_type = content_type
112
112
  res["Access-Control-Allow-Origin"] = "*"
113
+ extra_headers.each { |k, v| res[k] = v }
113
114
  res.body = body
114
115
  end
115
116
 
@@ -391,6 +391,10 @@ module Clacky
391
391
  return nil
392
392
  end
393
393
 
394
+ avatar_rel = spec["avatar"].to_s
395
+ avatar_abs = avatar_rel.empty? ? "" : File.join(container[:dir], avatar_rel)
396
+ avatar_abs = "" unless !avatar_abs.empty? && File.file?(avatar_abs)
397
+
394
398
  Unit.new(kind: :agent, id: spec["id"].to_s, ext_id: ext_id,
395
399
  layer: container[:layer], origin: container[:origin],
396
400
  dir: container[:dir],
@@ -399,6 +403,8 @@ module Clacky
399
403
  "title_zh" => spec["title_zh"].to_s,
400
404
  "prompt" => spec["prompt"],
401
405
  "prompt_abs" => prompt_abs,
406
+ "avatar" => avatar_rel,
407
+ "avatar_abs" => avatar_abs,
402
408
  "description" => spec["description"].to_s,
403
409
  "description_zh" => spec["description_zh"].to_s,
404
410
  "order" => spec["order"],
@@ -98,12 +98,6 @@ module Clacky
98
98
  # carrying encrypted skills is produced by the platform pipeline; refuse
99
99
  # to re-pack it so we never smuggle marketplace artifacts out of band.
100
100
  private def refuse_protected!(slug, container_dir)
101
- manifest = YAML.safe_load(File.read(File.join(container_dir, MANIFEST)), permitted_classes: [Symbol]) || {}
102
- origin = (manifest["origin"] || "self").to_s
103
- if origin == "marketplace"
104
- raise Error, "refusing to pack #{slug}: origin=marketplace is produced by the platform, not hand-packed"
105
- end
106
-
107
101
  enc = Dir.glob(File.join(container_dir, "**", "SKILL.md.enc"))
108
102
  return if enc.empty?
109
103
 
@@ -22,7 +22,7 @@ module Clacky
22
22
  PANEL_KEYS = %w[id title title_zh description description_zh view order attach].freeze
23
23
  API_KEYS = %w[id handler].freeze
24
24
  SKILL_KEYS = %w[id dir protected].freeze
25
- AGENT_KEYS = %w[id title title_zh description description_zh order prompt panels skills].freeze
25
+ AGENT_KEYS = %w[id title title_zh description description_zh order prompt panels skills avatar].freeze
26
26
  CHANNEL_KEYS = %w[id platform adapter].freeze
27
27
  PATCH_KEYS = %w[target file fingerprint on_mismatch].freeze
28
28
  HOOK_KEYS = %w[event file].freeze