openclacky 1.3.6 → 1.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +30 -0
  3. data/lib/clacky/agent.rb +8 -1
  4. data/lib/clacky/agent_profile.rb +121 -53
  5. data/lib/clacky/brand_config.rb +309 -6
  6. data/lib/clacky/cli.rb +4 -57
  7. data/lib/clacky/default_extensions/coding/ext.yml +18 -0
  8. data/lib/clacky/default_extensions/ext-studio/agents/ext-developer/system_prompt.md +95 -0
  9. data/lib/clacky/default_extensions/ext-studio/api/handler.rb +254 -0
  10. data/lib/clacky/default_extensions/ext-studio/ext.yml +32 -0
  11. data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +1269 -0
  12. data/lib/clacky/default_extensions/ext-studio/skills/ext-debug/SKILL.md +71 -0
  13. data/lib/clacky/default_extensions/ext-studio/skills/ext-publish/SKILL.md +74 -0
  14. data/lib/clacky/default_extensions/ext-studio/skills/ext-scaffold/SKILL.md +65 -0
  15. data/lib/clacky/default_extensions/general/ext.yml +18 -0
  16. data/lib/clacky/default_extensions/git/ext.yml +12 -0
  17. data/lib/clacky/{default_agents/_panels/git/panel.js → default_extensions/git/panels/git/view.js} +1 -2
  18. data/lib/clacky/default_extensions/meeting/{handler.rb → api/handler.rb} +78 -26
  19. data/lib/clacky/default_extensions/meeting/ext.yml +15 -0
  20. data/lib/clacky/default_extensions/meeting/{meeting.js → panels/meeting/view.js} +266 -89
  21. data/lib/clacky/default_extensions/meeting/skills/meeting-summarizer/SKILL.md +2 -1
  22. data/lib/clacky/default_extensions/time_machine/ext.yml +12 -0
  23. data/lib/clacky/{default_agents/_panels/time_machine/panel.js → default_extensions/time_machine/panels/time_machine/view.js} +58 -47
  24. data/lib/clacky/default_skills/channel-manager/discord_setup.rb +1 -1
  25. data/lib/clacky/default_skills/extend-openclacky/SKILL.md +83 -18
  26. data/lib/clacky/{api_extension.rb → extension/api_extension.rb} +27 -11
  27. data/lib/clacky/extension/api_loader.rb +136 -0
  28. data/lib/clacky/extension/cli_commands.rb +226 -0
  29. data/lib/clacky/{server/api_extension_dispatcher.rb → extension/dispatcher.rb} +22 -9
  30. data/lib/clacky/extension/hook_loader.rb +77 -0
  31. data/lib/clacky/extension/loader.rb +483 -0
  32. data/lib/clacky/extension/packager.rb +226 -0
  33. data/lib/clacky/{patch_loader.rb → extension/patch_loader.rb} +45 -0
  34. data/lib/clacky/extension/scaffold/template_renderer.rb +64 -0
  35. data/lib/clacky/extension/scaffold/templates/full/README.md.erb +48 -0
  36. data/lib/clacky/extension/scaffold/templates/full/agents/designer.md +5 -0
  37. data/lib/clacky/extension/scaffold/templates/full/api/handler.rb.erb +16 -0
  38. data/lib/clacky/extension/scaffold/templates/full/channels/noop.rb.erb +40 -0
  39. data/lib/clacky/extension/scaffold/templates/full/ext.yml.erb +41 -0
  40. data/lib/clacky/extension/scaffold/templates/full/hooks/audit.rb +9 -0
  41. data/lib/clacky/extension/scaffold/templates/full/panels/dashboard/view.js.erb +194 -0
  42. data/lib/clacky/extension/scaffold/templates/full/patches/audit.rb +15 -0
  43. data/lib/clacky/extension/scaffold/templates/full/skills/__slug__-skill/SKILL.md.erb +8 -0
  44. data/lib/clacky/extension/scaffold/templates/hello/api/handler.rb.erb +9 -0
  45. data/lib/clacky/extension/scaffold/templates/hello/ext.yml.erb +12 -0
  46. data/lib/clacky/extension/scaffold/templates/hello/panels/hello/view.js.erb +31 -0
  47. data/lib/clacky/extension/scaffold.rb +55 -0
  48. data/lib/clacky/extension/verifier.rb +196 -0
  49. data/lib/clacky/identity.rb +70 -0
  50. data/lib/clacky/locales/en.rb +19 -1
  51. data/lib/clacky/locales/zh.rb +19 -1
  52. data/lib/clacky/platform_http_client.rb +18 -15
  53. data/lib/clacky/server/channel/extension_adapter_loader.rb +32 -0
  54. data/lib/clacky/server/channel.rb +6 -0
  55. data/lib/clacky/server/http_server.rb +313 -144
  56. data/lib/clacky/server/session_registry.rb +9 -2
  57. data/lib/clacky/skill_loader.rb +28 -1
  58. data/lib/clacky/ui2/ui_controller.rb +9 -6
  59. data/lib/clacky/version.rb +1 -1
  60. data/lib/clacky/web/app.css +748 -573
  61. data/lib/clacky/web/app.js +63 -15
  62. data/lib/clacky/web/auth.js +2 -0
  63. data/lib/clacky/web/components/notify.js +2 -0
  64. data/lib/clacky/web/components/sidebar.js +1 -5
  65. data/lib/clacky/web/core/ext.js +316 -79
  66. data/lib/clacky/web/features/backup/store.js +1 -0
  67. data/lib/clacky/web/features/backup/view.js +0 -1
  68. data/lib/clacky/web/features/brand/view.js +13 -5
  69. data/lib/clacky/web/features/extensions/store.js +184 -0
  70. data/lib/clacky/web/features/extensions/view.js +389 -0
  71. data/lib/clacky/web/features/new-session/store.js +186 -0
  72. data/lib/clacky/web/features/new-session/view.js +319 -0
  73. data/lib/clacky/web/features/share/view.js +63 -14
  74. data/lib/clacky/web/features/skills/store.js +3 -1
  75. data/lib/clacky/web/features/skills/view.js +1 -1
  76. data/lib/clacky/web/features/tasks/view.js +12 -0
  77. data/lib/clacky/web/features/trash/store.js +24 -6
  78. data/lib/clacky/web/features/trash/view.js +46 -9
  79. data/lib/clacky/web/features/workspace/store.js +2 -0
  80. data/lib/clacky/web/features/workspace/view.js +0 -1
  81. data/lib/clacky/web/i18n.js +118 -106
  82. data/lib/clacky/web/index.html +138 -188
  83. data/lib/clacky/web/sessions.js +64 -269
  84. data/lib/clacky/web/settings.js +1 -1
  85. data/lib/clacky/web/skills.js +2 -0
  86. data/lib/clacky/web/theme.js +2 -0
  87. data/lib/clacky/web/ws-dispatcher.js +1 -0
  88. data/lib/clacky/web/ws.js +2 -0
  89. data/lib/clacky.rb +21 -10
  90. metadata +55 -24
  91. data/benchmark/runner.rb +0 -441
  92. data/lib/clacky/api_extension_loader.rb +0 -168
  93. data/lib/clacky/default_agents/SOUL.md +0 -3
  94. data/lib/clacky/default_agents/USER.md +0 -1
  95. data/lib/clacky/default_agents/coding/profile.yml +0 -5
  96. data/lib/clacky/default_agents/coding/webui/.gitkeep +0 -0
  97. data/lib/clacky/default_agents/general/profile.yml +0 -2
  98. data/lib/clacky/default_extensions/meeting/meta.yml +0 -3
  99. data/lib/clacky/web/features/creator/store.js +0 -81
  100. data/lib/clacky/web/features/creator/view.js +0 -380
  101. /data/lib/clacky/{default_agents → default_extensions/coding/agents}/coding/system_prompt.md +0 -0
  102. /data/lib/clacky/{default_agents → default_extensions/general/agents}/general/system_prompt.md +0 -0
  103. /data/lib/clacky/{default_agents/base_prompt.md → prompts/base.md} +0 -0
@@ -0,0 +1,1269 @@
1
+ // Extension & Creation — full-page workspace mounted from the sidebar.
2
+ // One rail entry opens a page (#ext/ext-studio) with two top tabs:
3
+ // • Extensions — local extension picker + verify (debug) + pack/publish
4
+ // • Skills — cloud/local skills, publish, iterate, create new
5
+ // Backend: extension side is /api/ext/ext-studio/; skill side reuses the host
6
+ // APIs /api/creator/skills and /api/my-skills/:name/publish.
7
+
8
+ (function () {
9
+ const STUDIO_I18N = {
10
+ en: {
11
+ "nav.entry": "Extension & Creation",
12
+ "ws.title": "Extension & Creation",
13
+ "tab.extensions": "Extensions",
14
+ "tab.skills": "Skills",
15
+ "ext.debug.section": "Debug",
16
+ "ext.publish.section": "Publish",
17
+ "debug.tab": "Debug",
18
+ "publish.tab": "Publish",
19
+ "picker.label": "Extension package",
20
+ "picker.empty": "No local extensions. Ask the AI to scaffold one.",
21
+ "detail.version": "Version",
22
+ "detail.layer": "Layer",
23
+ "detail.origin": "Origin",
24
+ "detail.units": "Contributed units",
25
+ "detail.noUnits": "No units",
26
+ "btn.recheck": "Re-check",
27
+ "btn.checking": "Checking…",
28
+ "verify.ok": "All checks passed.",
29
+ "verify.errors": "{{n}} error(s)",
30
+ "verify.warnings": "{{n}} warning(s)",
31
+ "verify.hint": "Hint",
32
+ "hint.reload": "After fixing, reload this page — extensions hot-reload per request.",
33
+ "publish.status": "Status",
34
+ "publish.status.draft": "Draft",
35
+ "publish.status.published": "Published",
36
+ "publish.changelog": "Changelog",
37
+ "publish.changelog.placeholder": "What changed in this version?",
38
+ "publish.force": "Publish a new version (already published)",
39
+ "btn.publish": "Publish to marketplace",
40
+ "btn.publishing": "Publishing…",
41
+ "btn.pack": "Pack (.zip)",
42
+ "publish.needLicense": "Publishing requires an activated user license.",
43
+ "publish.done": "Published {{id}} {{ver}} — {{status}}",
44
+ "publish.packed": "Packed → {{path}}",
45
+ "publish.already": "Already published. Enable \"publish a new version\" to ship.",
46
+ "published.title": "Your published extensions",
47
+ "published.empty": "You haven't published anything yet.",
48
+ "btn.unpublish": "Unpublish",
49
+ "published.confirm": "Unpublish {{id}} from the marketplace?",
50
+ "err.generic": "Something went wrong: {{msg}}",
51
+
52
+ "publish.confirm.title": "Publish to the marketplace?",
53
+ "publish.confirm.body": "\"{{id}}\" will be packed and uploaded to the OpenClacky marketplace, where anyone can discover and install it. Make sure it's ready to share publicly.",
54
+ "publish.confirm.ok": "Publish",
55
+ "publish.confirm.cancel": "Cancel",
56
+ "bind.title": "Authorize this device",
57
+ "bind.body": "Publishing requires a platform account. We'll open the OpenClacky authorization page — approve there, and this device will be linked to your account.",
58
+ "bind.starting": "Opening authorization page…",
59
+ "bind.pending": "Waiting for you to approve in the browser…",
60
+ "bind.code": "Verification code: {{code}}",
61
+ "bind.openLink": "Open authorization page",
62
+ "bind.success": "Device authorized. Publishing now…",
63
+ "bind.failed": "Authorization failed: {{msg}}",
64
+ "bind.denied": "Authorization was denied.",
65
+ "bind.expired": "Authorization expired. Please try again.",
66
+ "bind.cancel": "Cancel",
67
+
68
+ "pub.title.new": "Publish to the marketplace",
69
+ "pub.title.update": "Publish a new version",
70
+ "pub.intro": "\"{{name}}\" will be packed and uploaded to the OpenClacky marketplace, where anyone can discover and install it.",
71
+ "pub.version.new": "First release · v{{ver}}",
72
+ "pub.version.update": "v{{prev}} → v{{ver}}",
73
+ "pub.version.missing": "No version in ext.yml — add a \"version\" field before publishing.",
74
+ "pub.units": "Includes: {{units}}",
75
+ "pub.notes.label": "Release notes (optional)",
76
+ "pub.notes.placeholder": "What changed in this version? (README.md / CHANGELOG.md ship inside the package)",
77
+ "pub.readme.ok": "README.md detected",
78
+ "pub.readme.missing": "No README.md — consider adding one so users understand your extension.",
79
+ "pub.btn.publish": "Publish v{{ver}}",
80
+ "pub.btn.cancel": "Cancel",
81
+ "pub.progress.packing": "Packing…",
82
+ "pub.progress.uploading": "Uploading to marketplace…",
83
+ "pub.done": "Published {{id}} v{{ver}} — {{status}}",
84
+ "pub.done.close": "Done",
85
+ "pub.retry": "Try again",
86
+ "extlist.section.cloud": "Published Extensions",
87
+ "extlist.section.cloudHint": "Live on the marketplace",
88
+ "extlist.section.local": "Local Extensions",
89
+ "extlist.section.localHint": "Ready to publish",
90
+ "extlist.cloud.empty": "No extensions published yet.",
91
+ "extlist.local.empty": "No local extensions. Create one below.",
92
+ "extlist.badge.published": "Published",
93
+ "extlist.badge.draft": "Draft",
94
+ "extlist.badge.local": "Not published",
95
+ "extlist.verify.ok": "Checks passed",
96
+ "extlist.verify.errors": "{{n}} error(s)",
97
+ "extlist.verify.warnings": "{{n}} warning(s)",
98
+ "extlist.btn.publish": "Publish",
99
+ "extlist.btn.update": "Update",
100
+ "extlist.btn.pack": "Pack",
101
+ "extlist.btn.unpublish": "Unpublish",
102
+ "extlist.btn.iterate": "Iterate",
103
+ "extlist.iterate.seed": "Iterate on extension {{id}}",
104
+ "extlist.changelog.prompt": "Changelog (optional):",
105
+ "extlist.overwrite.confirm": "\"{{id}}\" is already published. Publish a new version?",
106
+ "extlist.unpublish.confirm": "Unpublish {{id}} from the marketplace?",
107
+ "extlist.publishing": "Publishing…",
108
+ "extlist.packed": "Packed → {{path}}",
109
+ "extlist.needLicense": "Publishing requires an activated user license.",
110
+ "extlist.newExt.label": "Create a new extension",
111
+ "extlist.newExt.hint": "Opens an AI session that scaffolds and builds it for you.",
112
+ "extlist.newExt.btn": "Create New Extension",
113
+ "extlist.newExt.seed": "I want to build a new OpenClacky extension. Help me get started: ask me what the extension should do, then scaffold it and build it out step by step.",
114
+ "skills.section.cloud": "Cloud Skills",
115
+ "skills.section.cloudHint": "Published to the platform",
116
+ "skills.section.local": "Local Skills",
117
+ "skills.section.localHint": "Ready to publish",
118
+ "skills.cloud.empty": "No skills published yet.",
119
+ "skills.cloud.locked": "Become a creator to upload and publish skills.",
120
+ "skills.local.empty": "All local skills are already published.",
121
+ "skills.badge.published": "Published",
122
+ "skills.badge.unpublished": "Not published",
123
+ "skills.changed": "Has local changes",
124
+ "skills.hasLocalChanges": "Local SKILL.md is newer than the last upload",
125
+ "skills.downloads": "Downloads",
126
+ "skills.btn.publish": "Publish",
127
+ "skills.btn.update": "Update",
128
+ "skills.btn.upToDate": "Up to date",
129
+ "skills.btn.iterate": "Iterate",
130
+ "skills.iterate.prompt": "Update skill:",
131
+ "skills.shadow.label": "Local override",
132
+ "skills.shadow.tooltip": "Local copy shadows a same-named brand skill",
133
+ "skills.newSkill.label": "Create a new skill with /skill-creator",
134
+ "skills.newSkill.btn": "Create New Skill",
135
+ "skills.promo.text": "Publish your skills & build your own brand on OpenClacky.",
136
+ "skills.promo.link": "Learn more →",
137
+ "skills.locked": "Creator license required to publish cloud skills.",
138
+ "skills.publishing": "Publishing…",
139
+ },
140
+ zh: {
141
+ "nav.entry": "扩展与创作",
142
+ "ws.title": "扩展与创作",
143
+ "tab.extensions": "扩展",
144
+ "tab.skills": "创作",
145
+ "ext.debug.section": "调试",
146
+ "ext.publish.section": "发布",
147
+ "debug.tab": "调试",
148
+ "publish.tab": "发布",
149
+ "picker.label": "扩展包",
150
+ "picker.empty": "本地暂无扩展。让 AI 帮你生成一个。",
151
+ "detail.version": "版本",
152
+ "detail.layer": "层级",
153
+ "detail.origin": "来源",
154
+ "detail.units": "贡献单元",
155
+ "detail.noUnits": "暂无单元",
156
+ "btn.recheck": "重新检查",
157
+ "btn.checking": "检查中…",
158
+ "verify.ok": "全部检查通过。",
159
+ "verify.errors": "{{n}} 个错误",
160
+ "verify.warnings": "{{n}} 个警告",
161
+ "verify.hint": "提示",
162
+ "hint.reload": "修复后刷新本页 —— 扩展每次请求热重载。",
163
+ "publish.status": "状态",
164
+ "publish.status.draft": "草稿",
165
+ "publish.status.published": "已发布",
166
+ "publish.changelog": "更新说明",
167
+ "publish.changelog.placeholder": "这个版本改了什么?",
168
+ "publish.force": "发布新版本(已发布过)",
169
+ "btn.publish": "发布到市场",
170
+ "btn.publishing": "发布中…",
171
+ "btn.pack": "打包 (.zip)",
172
+ "publish.needLicense": "发布需要已激活的用户授权。",
173
+ "publish.done": "已发布 {{id}} {{ver}} — {{status}}",
174
+ "publish.packed": "已打包 → {{path}}",
175
+ "publish.already": "已发布过。勾选「发布新版本」后再试。",
176
+ "published.title": "你发布的扩展",
177
+ "published.empty": "你还没有发布任何扩展。",
178
+ "btn.unpublish": "下架",
179
+ "published.confirm": "确定要从市场下架 {{id}} 吗?",
180
+ "err.generic": "出错了:{{msg}}",
181
+
182
+ "publish.confirm.title": "确定发布到市场?",
183
+ "publish.confirm.body": "「{{id}}」将被打包并上传到 OpenClacky 市场,任何人都能发现并安装它。请确认它已准备好公开分享。",
184
+ "publish.confirm.ok": "发布",
185
+ "publish.confirm.cancel": "取消",
186
+ "bind.title": "授权此设备",
187
+ "bind.body": "发布需要平台账号。我们将打开 OpenClacky 授权页面——在那里确认后,此设备就会关联到你的账号。",
188
+ "bind.starting": "正在打开授权页面…",
189
+ "bind.pending": "等待你在浏览器中确认授权…",
190
+ "bind.code": "验证码:{{code}}",
191
+ "bind.openLink": "打开授权页面",
192
+ "bind.success": "设备已授权,正在发布…",
193
+ "bind.failed": "授权失败:{{msg}}",
194
+ "bind.denied": "授权被拒绝。",
195
+ "bind.expired": "授权已过期,请重试。",
196
+ "bind.cancel": "取消",
197
+
198
+ "pub.title.new": "发布到市场",
199
+ "pub.title.update": "发布新版本",
200
+ "pub.intro": "「{{name}}」将被打包并上传到 OpenClacky 市场,任何人都能发现并安装它。",
201
+ "pub.version.new": "首次发布 · v{{ver}}",
202
+ "pub.version.update": "v{{prev}} → v{{ver}}",
203
+ "pub.version.missing": "ext.yml 里没有 version — 请先补上 version 字段再发布。",
204
+ "pub.units": "包含:{{units}}",
205
+ "pub.notes.label": "更新说明(可选)",
206
+ "pub.notes.placeholder": "这个版本改了什么?(README.md / CHANGELOG.md 会随包一起发布)",
207
+ "pub.readme.ok": "已检测到 README.md",
208
+ "pub.readme.missing": "未找到 README.md — 建议补一份,方便用户了解你的扩展。",
209
+ "pub.btn.publish": "发布 v{{ver}}",
210
+ "pub.btn.cancel": "取消",
211
+ "pub.progress.packing": "打包中…",
212
+ "pub.progress.uploading": "上传到市场…",
213
+ "pub.done": "已发布 {{id}} v{{ver}} — {{status}}",
214
+ "pub.done.close": "完成",
215
+ "pub.retry": "重试",
216
+ "extlist.section.cloud": "已发布扩展",
217
+ "extlist.section.cloudHint": "已上架到市场",
218
+ "extlist.section.local": "本地扩展",
219
+ "extlist.section.localHint": "可发布",
220
+ "extlist.cloud.empty": "还没有发布任何扩展。",
221
+ "extlist.local.empty": "本地暂无扩展,在下方新建一个。",
222
+ "extlist.badge.published": "已发布",
223
+ "extlist.badge.draft": "草稿",
224
+ "extlist.badge.local": "未发布",
225
+ "extlist.verify.ok": "检查通过",
226
+ "extlist.verify.errors": "{{n}} 个错误",
227
+ "extlist.verify.warnings": "{{n}} 个警告",
228
+ "extlist.btn.publish": "发布",
229
+ "extlist.btn.update": "更新",
230
+ "extlist.btn.pack": "打包",
231
+ "extlist.btn.unpublish": "下架",
232
+ "extlist.btn.iterate": "迭代",
233
+ "extlist.iterate.seed": "迭代扩展 {{id}}",
234
+ "extlist.changelog.prompt": "更新说明(可选):",
235
+ "extlist.overwrite.confirm": "「{{id}}」已经发布过了。要发布新版本吗?",
236
+ "extlist.unpublish.confirm": "确定要从市场下架 {{id}} 吗?",
237
+ "extlist.publishing": "发布中…",
238
+ "extlist.packed": "已打包 → {{path}}",
239
+ "extlist.needLicense": "发布需要已激活的用户授权。",
240
+ "extlist.newExt.label": "新建扩展",
241
+ "extlist.newExt.hint": "打开一个 AI 会话,帮你生成并开发它。",
242
+ "extlist.newExt.btn": "新建扩展",
243
+ "extlist.newExt.seed": "我想开发一个新的 OpenClacky 扩展。请引导我开始:先问清楚这个扩展要做什么,然后帮我搭好骨架并一步步开发出来。",
244
+ "skills.section.cloud": "云端 Skills",
245
+ "skills.section.cloudHint": "已发布到平台",
246
+ "skills.section.local": "本地 Skills",
247
+ "skills.section.localHint": "可发布",
248
+ "skills.cloud.empty": "还没有发布任何 skill。",
249
+ "skills.cloud.locked": "成为创作者后才能上传并发布 skill。",
250
+ "skills.local.empty": "所有本地 skill 都已发布。",
251
+ "skills.badge.published": "已发布",
252
+ "skills.badge.unpublished": "未发布",
253
+ "skills.changed": "有本地改动",
254
+ "skills.hasLocalChanges": "本地 SKILL.md 比上次上传更新",
255
+ "skills.downloads": "下载量",
256
+ "skills.btn.publish": "发布",
257
+ "skills.btn.update": "更新",
258
+ "skills.btn.upToDate": "已是最新",
259
+ "skills.btn.iterate": "迭代",
260
+ "skills.iterate.prompt": "更新 skill:",
261
+ "skills.shadow.label": "本地覆盖",
262
+ "skills.shadow.tooltip": "本地副本覆盖了同名品牌 skill",
263
+ "skills.newSkill.label": "用 /skill-creator 创建新 skill",
264
+ "skills.newSkill.btn": "创建新 Skill",
265
+ "skills.promo.text": "发布你的 skill,在 OpenClacky 上打造自己的品牌。",
266
+ "skills.promo.link": "了解更多 →",
267
+ "skills.locked": "发布云端 skill 需要创作者授权。",
268
+ "skills.publishing": "发布中…",
269
+ },
270
+ };
271
+
272
+ function t(key, vars) {
273
+ const lang = (typeof I18n !== "undefined" && I18n.lang && I18n.lang()) || "en";
274
+ const dict = STUDIO_I18N[lang] || STUDIO_I18N.en;
275
+ let str = dict[key] != null ? dict[key] : (STUDIO_I18N.en[key] != null ? STUDIO_I18N.en[key] : key);
276
+ if (vars) Object.keys(vars).forEach((k) => { str = str.split("{{" + k + "}}").join(vars[k]); });
277
+ return str;
278
+ }
279
+
280
+ function api(path) { return `/api/ext/ext-studio${path}`; }
281
+
282
+ async function getJson(path) {
283
+ const res = await fetch(api(path));
284
+ const data = await res.json();
285
+ if (!res.ok) throw new Error(data.error || `Request failed (${res.status})`);
286
+ return data;
287
+ }
288
+
289
+ async function postJson(path, body) {
290
+ const res = await fetch(api(path), {
291
+ method: "POST",
292
+ headers: { "Content-Type": "application/json" },
293
+ body: JSON.stringify(body || {}),
294
+ });
295
+ const data = await res.json();
296
+ if (!res.ok) {
297
+ const err = new Error(data.error || `Request failed (${res.status})`);
298
+ err.status = res.status;
299
+ err.data = data;
300
+ throw err;
301
+ }
302
+ return data;
303
+ }
304
+
305
+ // Skills tab talks to host-owned endpoints (not the ext prefix). We surface
306
+ // the HTTP status so the caller can treat 403 as "locked" rather than error.
307
+ async function getHost(path) {
308
+ const res = await fetch(path);
309
+ let data = {};
310
+ try { data = await res.json(); } catch (_e) {}
311
+ return { status: res.status, ok: res.ok, data };
312
+ }
313
+
314
+ function el(tag, attrs, children) {
315
+ const node = document.createElement(tag);
316
+ if (attrs) Object.keys(attrs).forEach((k) => {
317
+ if (k === "class") node.className = attrs[k];
318
+ else if (k === "text") node.textContent = attrs[k];
319
+ else if (k.startsWith("on") && typeof attrs[k] === "function") node.addEventListener(k.slice(2), attrs[k]);
320
+ else node.setAttribute(k, attrs[k]);
321
+ });
322
+ (children || []).forEach((c) => { if (c) node.appendChild(typeof c === "string" ? document.createTextNode(c) : c); });
323
+ return node;
324
+ }
325
+
326
+ // ── Shared: modal dialog ───────────────────────────────────────────────
327
+ // A minimal overlay modal used by the publish-confirm and device-binding
328
+ // flows. Returns the body element so callers can drive it live (bind flow),
329
+ // plus a close() handle. Footer buttons wire straight to callbacks.
330
+ function openModal({ title, body, buttons }) {
331
+ const overlay = el("div", { class: "studio-modal-overlay" });
332
+ const box = el("div", { class: "studio-modal" });
333
+ const bodyEl = el("div", { class: "studio-modal-body" });
334
+ if (typeof body === "string") bodyEl.textContent = body; else if (body) bodyEl.appendChild(body);
335
+
336
+ const footer = el("div", { class: "studio-modal-footer" });
337
+ const close = () => { if (overlay.parentNode) overlay.parentNode.removeChild(overlay); };
338
+ (buttons || []).forEach((b) => {
339
+ footer.appendChild(el("button", {
340
+ class: "studio-btn" + (b.primary ? " studio-btn-primary" : ""),
341
+ text: b.label,
342
+ onclick: () => { if (!b.keepOpen) close(); if (b.onClick) b.onClick(); },
343
+ }));
344
+ });
345
+
346
+ box.appendChild(el("h3", { class: "studio-modal-title", text: title }));
347
+ box.appendChild(bodyEl);
348
+ box.appendChild(footer);
349
+ overlay.appendChild(box);
350
+ overlay.addEventListener("click", (e) => { if (e.target === overlay) close(); });
351
+ document.body.appendChild(overlay);
352
+ return { overlay, bodyEl, footer, close };
353
+ }
354
+
355
+ function confirmModal({ title, body, okLabel, cancelLabel }) {
356
+ return new Promise((resolve) => {
357
+ openModal({
358
+ title,
359
+ body,
360
+ buttons: [
361
+ { label: cancelLabel, onClick: () => resolve(false) },
362
+ { label: okLabel, primary: true, onClick: () => resolve(true) },
363
+ ],
364
+ });
365
+ });
366
+ }
367
+
368
+ // ── Shared: device-binding flow ────────────────────────────────────────
369
+ // Runs the RFC 8628 device-authorization flow: opens the platform's
370
+ // verification page and polls until approval. Resolves true once this device
371
+ // is bound to a platform account, false on cancel/denial/expiry.
372
+ function runBindingFlow() {
373
+ return new Promise((resolve) => {
374
+ let polling = false;
375
+ let popup = null;
376
+
377
+ const status = el("p", { class: "studio-modal-status", text: t("bind.body") });
378
+ const codeLine = el("p", { class: "studio-modal-code" });
379
+ const link = el("a", { class: "studio-modal-link", target: "_blank", rel: "noopener", text: t("bind.openLink") });
380
+ link.style.display = "none";
381
+
382
+ const modal = openModal({
383
+ title: t("bind.title"),
384
+ body: el("div", null, [status, codeLine, link]),
385
+ buttons: [{ label: t("bind.cancel"), keepOpen: false, onClick: () => { polling = false; resolve(false); } }],
386
+ });
387
+
388
+ function finish(ok) { polling = false; modal.close(); resolve(ok); }
389
+
390
+ async function poll(deviceCode, intervalMs) {
391
+ while (polling) {
392
+ await new Promise((r) => setTimeout(r, intervalMs));
393
+ if (!polling) return;
394
+ let data;
395
+ try { data = await postJson("/binding/poll", { device_code: deviceCode }); }
396
+ catch (_e) { continue; }
397
+ if (data.status === "approved") { status.textContent = t("bind.success"); setTimeout(() => finish(true), 600); return; }
398
+ if (data.status === "pending") continue;
399
+ const msg = data.status === "denied" ? t("bind.denied")
400
+ : data.status === "expired" ? t("bind.expired")
401
+ : t("bind.failed", { msg: data.error || data.status || "" });
402
+ status.textContent = msg;
403
+ return;
404
+ }
405
+ }
406
+
407
+ (async () => {
408
+ status.textContent = t("bind.starting");
409
+ popup = window.open("about:blank", "_blank");
410
+ let data;
411
+ try { data = await postJson("/binding/start", {}); }
412
+ catch (e) {
413
+ if (popup && !popup.closed) popup.close();
414
+ status.textContent = t("bind.failed", { msg: e.message });
415
+ return;
416
+ }
417
+ const url = data.verification_uri_complete || data.verification_uri;
418
+ codeLine.textContent = data.user_code ? t("bind.code", { code: data.user_code }) : "";
419
+ if (url) { link.href = url; link.style.display = ""; }
420
+ status.textContent = t("bind.pending");
421
+ if (popup && !popup.closed) popup.location.href = url; else if (url) window.open(url, "_blank");
422
+ polling = true;
423
+ poll(data.device_code, (data.interval || 5) * 1000);
424
+ })();
425
+ });
426
+ }
427
+
428
+ // Publish an extension, transparently running the device-binding flow when the
429
+ // backend reports the device isn't bound yet (HTTP 428), then retrying once.
430
+ // Returns the publish response, or null if the user cancelled binding.
431
+ async function publishWithBinding(body) {
432
+ try {
433
+ return await postJson("/publish", body);
434
+ } catch (e) {
435
+ if (e.status === 428 && e.data && e.data.needs_binding) {
436
+ const bound = await runBindingFlow();
437
+ if (!bound) return null;
438
+ return await postJson("/publish", body);
439
+ }
440
+ throw e;
441
+ }
442
+ }
443
+
444
+ // Unified publish flow: one modal that walks the creator from a release form
445
+ // (version + notes) through progress → device binding (if needed) → done.
446
+ // The creator owns the version in ext.yml; a new release must be greater than
447
+ // the currently published version — the backend enforces that. `prevVersion`
448
+ // is the latest published version when updating, or null for a first release.
449
+ // Resolves true when a publish succeeded, false otherwise.
450
+ function runPublishFlow(ext, prevVersion) {
451
+ return new Promise((resolve) => {
452
+ const version = ext.version;
453
+ const isUpdate = !!prevVersion;
454
+
455
+ const intro = el("p", { class: "studio-modal-status", text: t("pub.intro", { name: ext.name }) });
456
+
457
+ const verText = !version
458
+ ? t("pub.version.missing")
459
+ : isUpdate
460
+ ? t("pub.version.update", { prev: prevVersion, ver: version })
461
+ : t("pub.version.new", { ver: version });
462
+ const verLine = el("p", { class: "studio-modal-code", text: verText });
463
+
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;
468
+
469
+ const notesLabel = el("label", { class: "studio-label", text: t("pub.notes.label") });
470
+ const notes = el("textarea", { class: "studio-textarea", rows: "3", placeholder: t("pub.notes.placeholder") });
471
+
472
+ const status = el("p", { class: "studio-modal-status" });
473
+ status.style.display = "none";
474
+
475
+ const bodyChildren = [intro, verLine];
476
+ if (unitsLine) bodyChildren.push(unitsLine);
477
+ bodyChildren.push(notesLabel, notes, status);
478
+
479
+ let done = false;
480
+ const modal = openModal({
481
+ title: isUpdate ? t("pub.title.update") : t("pub.title.new"),
482
+ body: el("div", null, bodyChildren),
483
+ buttons: [
484
+ { label: t("pub.btn.cancel"), keepOpen: false, onClick: () => { if (!done) resolve(false); } },
485
+ {
486
+ label: version ? t("pub.btn.publish", { ver: version }) : t("pub.btn.publish", { ver: "?" }),
487
+ primary: true,
488
+ keepOpen: true,
489
+ onClick: () => submit(),
490
+ },
491
+ ],
492
+ });
493
+
494
+ const publishBtn = modal.footer.querySelector(".studio-btn-primary");
495
+ const cancelBtn = modal.footer.querySelector(".studio-btn:not(.studio-btn-primary)");
496
+ if (!version) publishBtn.disabled = true;
497
+
498
+ function setProgress(msg) {
499
+ status.style.display = "";
500
+ status.textContent = msg;
501
+ }
502
+
503
+ async function submit() {
504
+ if (done || publishBtn.disabled) return;
505
+ publishBtn.disabled = true;
506
+ cancelBtn.disabled = true;
507
+ notes.disabled = true;
508
+ setProgress(t("pub.progress.packing"));
509
+ try {
510
+ setProgress(t("pub.progress.uploading"));
511
+ const data = await publishWithBinding({
512
+ ext_id: ext.id,
513
+ force: isUpdate,
514
+ changelog: notes.value.trim(),
515
+ });
516
+ if (data === null) {
517
+ publishBtn.disabled = false;
518
+ cancelBtn.disabled = false;
519
+ notes.disabled = false;
520
+ status.style.display = "none";
521
+ return;
522
+ }
523
+ if (!data.ok) throw new Error(data.error || "Publish failed");
524
+ done = true;
525
+ setProgress(t("pub.done", {
526
+ id: data.ext_id,
527
+ ver: data.version || version,
528
+ status: data.status || "",
529
+ }));
530
+ publishBtn.style.display = "none";
531
+ cancelBtn.textContent = t("pub.done.close");
532
+ cancelBtn.disabled = false;
533
+ cancelBtn.onclick = () => { modal.close(); resolve(true); };
534
+ } catch (e) {
535
+ setProgress(t("err.generic", { msg: e.message }));
536
+ publishBtn.disabled = false;
537
+ publishBtn.textContent = t("pub.retry");
538
+ cancelBtn.disabled = false;
539
+ notes.disabled = false;
540
+ }
541
+ }
542
+ });
543
+ }
544
+
545
+ // ── Shared: extension list + current selection ─────────────────────────
546
+ // Both tabs care about "which local extension am I working on", so we keep a
547
+ // tiny page-level store and let each tab subscribe to changes.
548
+ const store = {
549
+ extensions: [],
550
+ selectedId: null,
551
+ loaded: false,
552
+ listeners: new Set(),
553
+ subscribe(fn) { this.listeners.add(fn); return () => this.listeners.delete(fn); },
554
+ notify() { this.listeners.forEach((fn) => fn()); },
555
+ selected() { return this.extensions.find((e) => e.id === this.selectedId) || null; },
556
+ async reload() {
557
+ const data = await getJson("/extensions");
558
+ this.extensions = data.extensions || [];
559
+ this.loaded = true;
560
+ if (!this.selectedId || !this.extensions.some((e) => e.id === this.selectedId)) {
561
+ this.selectedId = this.extensions.length ? this.extensions[0].id : null;
562
+ }
563
+ this.notify();
564
+ },
565
+ };
566
+
567
+ function renderPicker(onChange) {
568
+ const wrap = el("div", { class: "studio-field" });
569
+ wrap.appendChild(el("label", { class: "studio-label", text: t("picker.label") }));
570
+ if (!store.extensions.length) {
571
+ wrap.appendChild(el("p", { class: "studio-empty", text: t("picker.empty") }));
572
+ return wrap;
573
+ }
574
+ const select = el("select", { class: "studio-select" });
575
+ store.extensions.forEach((e) => {
576
+ const opt = el("option", { value: e.id, text: `${e.name} (${e.id})` });
577
+ if (e.id === store.selectedId) opt.selected = true;
578
+ select.appendChild(opt);
579
+ });
580
+ select.addEventListener("change", () => { store.selectedId = select.value; store.notify(); if (onChange) onChange(); });
581
+ wrap.appendChild(select);
582
+ return wrap;
583
+ }
584
+
585
+ // ── Debug tab ──────────────────────────────────────────────────────────
586
+ function createDebugPanel() {
587
+ let container = null;
588
+ let unsub = null;
589
+
590
+ async function runVerify() {
591
+ const ext = store.selected();
592
+ if (!ext) return;
593
+ const status = container.querySelector(".studio-verify-status");
594
+ if (status) status.textContent = t("btn.checking");
595
+ try {
596
+ const data = await postJson("/verify", { ext_id: ext.id });
597
+ renderVerify(data);
598
+ } catch (e) {
599
+ renderError(e);
600
+ }
601
+ }
602
+
603
+ function renderVerify(data) {
604
+ const box = container.querySelector(".studio-verify");
605
+ if (!box) return;
606
+ box.innerHTML = "";
607
+ const errs = (data.issues || []).filter((i) => i.level === "error");
608
+ const warns = (data.issues || []).filter((i) => i.level === "warning");
609
+
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(" · ");
618
+ }
619
+ box.appendChild(summary);
620
+
621
+ (data.issues || []).forEach((i) => {
622
+ const item = el("div", { class: "studio-issue studio-issue-" + i.level });
623
+ item.appendChild(el("div", { class: "studio-issue-code", text: `${i.code}${i.unit ? " · " + i.unit : ""}` }));
624
+ item.appendChild(el("div", { class: "studio-issue-msg", text: i.message }));
625
+ if (i.file) item.appendChild(el("div", { class: "studio-issue-file", text: i.file }));
626
+ if (i.hint) item.appendChild(el("div", { class: "studio-issue-hint", text: t("verify.hint") + ": " + i.hint }));
627
+ box.appendChild(item);
628
+ });
629
+ }
630
+
631
+ function renderError(e) {
632
+ 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 }) })); }
634
+ }
635
+
636
+ function renderDetail() {
637
+ const detail = container.querySelector(".studio-detail");
638
+ if (!detail) return;
639
+ detail.innerHTML = "";
640
+ const ext = store.selected();
641
+ if (!ext) return;
642
+
643
+ detail.appendChild(el("h4", { class: "studio-detail-name", text: ext.name }));
644
+ if (ext.description) detail.appendChild(el("p", { class: "studio-detail-desc", text: ext.description }));
645
+
646
+ const meta = el("dl", { class: "studio-meta" });
647
+ const row = (label, value) => { meta.appendChild(el("dt", { text: label })); meta.appendChild(el("dd", { text: value || "—" })); };
648
+ row(t("detail.version"), ext.version);
649
+ row(t("detail.layer"), ext.layer);
650
+ row(t("detail.origin"), ext.origin);
651
+ detail.appendChild(meta);
652
+
653
+ detail.appendChild(el("div", { class: "studio-label", text: t("detail.units") }));
654
+ if (!ext.units.length) {
655
+ detail.appendChild(el("p", { class: "studio-empty", text: t("detail.noUnits") }));
656
+ } else {
657
+ const list = el("div", { class: "studio-units" });
658
+ ext.units.forEach((u) => list.appendChild(el("span", { class: "studio-unit-chip", text: `${u.kind} · ${u.id}` })));
659
+ detail.appendChild(list);
660
+ }
661
+ }
662
+
663
+ function rebuild() {
664
+ if (!container) return;
665
+ container.innerHTML = "";
666
+ container.appendChild(renderPicker(() => { rebuild(); runVerify(); }));
667
+
668
+ if (!store.selected()) {
669
+ container.appendChild(el("p", { class: "studio-hint", text: t("hint.reload") }));
670
+ return;
671
+ }
672
+
673
+ container.appendChild(el("div", { class: "studio-detail" }));
674
+
675
+ const bar = el("div", { class: "studio-actions" });
676
+ bar.appendChild(el("button", { class: "studio-btn studio-btn-primary studio-verify-status", text: t("btn.recheck"), onclick: runVerify }));
677
+ container.appendChild(bar);
678
+
679
+ container.appendChild(el("div", { class: "studio-verify" }));
680
+ container.appendChild(el("p", { class: "studio-hint", text: t("hint.reload") }));
681
+
682
+ renderDetail();
683
+ runVerify();
684
+ }
685
+
686
+ return {
687
+ async attach(root) {
688
+ container = el("div", { class: "studio-panel" });
689
+ root.appendChild(container);
690
+ unsub = store.subscribe(rebuild);
691
+ container.appendChild(el("p", { class: "studio-hint", text: "…" }));
692
+ try { if (!store.loaded) await store.reload(); rebuild(); }
693
+ catch (e) { renderError(e); }
694
+ },
695
+ destroy() { if (unsub) unsub(); },
696
+ };
697
+ }
698
+
699
+ // ── Publish tab ──────────────────────────────────────────────────────────
700
+ function createPublishPanel() {
701
+ let container = null;
702
+ let unsub = null;
703
+
704
+ function feedback(msg, kind) {
705
+ const box = container.querySelector(".studio-feedback");
706
+ if (box) { box.className = "studio-feedback studio-feedback-" + (kind || "info"); box.textContent = msg; }
707
+ }
708
+
709
+ async function doPublish() {
710
+ const ext = store.selected();
711
+ if (!ext) return;
712
+ let prevVersion = null;
713
+ try {
714
+ const data = await getJson("/published");
715
+ const match = (data.extensions || []).find((e) => e.id === ext.id);
716
+ prevVersion = match ? match.version : null;
717
+ } catch (_e) { /* treat as first release */ }
718
+ const ok = await runPublishFlow(ext, prevVersion);
719
+ if (ok) loadPublished();
720
+ }
721
+
722
+ async function doPack() {
723
+ const ext = store.selected();
724
+ if (!ext) return;
725
+ try {
726
+ const data = await postJson("/pack", { ext_id: ext.id });
727
+ feedback(t("publish.packed", { path: data.path }), "success");
728
+ } catch (e) {
729
+ feedback(t("err.generic", { msg: e.message }), "error");
730
+ }
731
+ }
732
+
733
+ async function loadPublished() {
734
+ const box = container.querySelector(".studio-published");
735
+ if (!box) return;
736
+ box.innerHTML = "";
737
+ box.appendChild(el("div", { class: "studio-label", text: t("published.title") }));
738
+ try {
739
+ const data = await getJson("/published");
740
+ const exts = data.extensions || [];
741
+ if (!exts.length) { box.appendChild(el("p", { class: "studio-empty", text: t("published.empty") })); return; }
742
+ exts.forEach((e) => {
743
+ const row = el("div", { class: "studio-published-row" });
744
+ row.appendChild(el("span", { class: "studio-published-name", text: `${e.name}${e.version ? " v" + e.version : ""} [${e.status}]` }));
745
+ row.appendChild(el("button", {
746
+ class: "studio-btn studio-btn-danger", text: t("btn.unpublish"),
747
+ onclick: async () => {
748
+ if (!window.confirm(t("published.confirm", { id: e.id }))) return;
749
+ try { await postJson("/unpublish", { ext_id: e.id }); loadPublished(); }
750
+ catch (err) { feedback(t("err.generic", { msg: err.message }), "error"); }
751
+ },
752
+ }));
753
+ box.appendChild(row);
754
+ });
755
+ } catch (e) {
756
+ box.appendChild(el("p", { class: "studio-empty", text: t("err.generic", { msg: e.message }) }));
757
+ }
758
+ }
759
+
760
+ function rebuild() {
761
+ if (!container) return;
762
+ container.innerHTML = "";
763
+ container.appendChild(renderPicker(rebuild));
764
+
765
+ if (!store.selected()) return;
766
+
767
+ const bar = el("div", { class: "studio-actions" });
768
+ 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 }));
770
+ container.appendChild(bar);
771
+
772
+ container.appendChild(el("div", { class: "studio-feedback" }));
773
+ container.appendChild(el("div", { class: "studio-published" }));
774
+
775
+ loadPublished();
776
+ }
777
+
778
+ return {
779
+ async attach(root) {
780
+ container = el("div", { class: "studio-panel" });
781
+ root.appendChild(container);
782
+ unsub = store.subscribe(rebuild);
783
+ try { if (!store.loaded) await store.reload(); rebuild(); }
784
+ catch (e) { container.appendChild(el("p", { class: "studio-empty", text: t("err.generic", { msg: e.message }) })); }
785
+ },
786
+ destroy() { if (unsub) unsub(); },
787
+ };
788
+ }
789
+
790
+ // ── Extensions tab (full-page): cloud + local extension cards ──────────────
791
+ // Reuses backend endpoints GET /published (cloud), the shared `store` (local,
792
+ // GET /extensions), POST /publish|/pack|/unpublish, and POST /develop to open
793
+ // an AI build session. Mirrors the Skills tab layout.
794
+ function createExtensionsPanel() {
795
+ let container = null;
796
+ let cloud = [];
797
+
798
+ function licensed() {
799
+ return !(typeof Brand !== "undefined" && Brand.branded && !Brand.userLicensed);
800
+ }
801
+
802
+ async function reload() {
803
+ await store.reload();
804
+ try {
805
+ const data = await getJson("/published");
806
+ cloud = data.extensions || [];
807
+ } catch (_e) {
808
+ cloud = [];
809
+ }
810
+ }
811
+
812
+ function createExtension(idea) {
813
+ if (idea === null) return; // user cancelled the prompt
814
+ const prompt = idea.trim() ? idea.trim() : null;
815
+ postJson("/develop", { idea: prompt })
816
+ .then((data) => { if (data && data.session_id) window.Clacky.Router.navigate("session", { id: data.session_id }); })
817
+ .catch((e) => alert(t("err.generic", { msg: e.message })));
818
+ }
819
+
820
+ function badge(text, kind) {
821
+ return el("span", { class: "studio-skill-badge studio-skill-badge-" + kind, text });
822
+ }
823
+
824
+ function cloudCard(ext) {
825
+ const card = el("div", { class: "studio-skill-card" });
826
+ const head = el("div", { class: "studio-skill-head" });
827
+ head.appendChild(el("span", { class: "studio-skill-name", text: ext.name || ext.id }));
828
+ const isDraft = ext.status === "draft";
829
+ head.appendChild(badge(isDraft ? t("extlist.badge.draft") : t("extlist.badge.published"), isDraft ? "local" : "published"));
830
+ card.appendChild(head);
831
+
832
+ const meta = el("div", { class: "studio-skill-meta" });
833
+ if (ext.version) meta.appendChild(el("span", { text: "v" + ext.version }));
834
+ card.appendChild(meta);
835
+
836
+ const actions = el("div", { class: "studio-actions" });
837
+ actions.appendChild(el("button", { class: "studio-btn studio-btn-primary", text: t("extlist.btn.iterate"), onclick: () => createExtension(t("extlist.iterate.seed", { id: ext.id })) }));
838
+ const un = el("button", { class: "studio-btn studio-btn-ghost", text: t("extlist.btn.unpublish") });
839
+ un.addEventListener("click", async () => {
840
+ if (!window.confirm(t("extlist.unpublish.confirm", { id: ext.id }))) return;
841
+ un.disabled = true;
842
+ try { await postJson("/unpublish", { ext_id: ext.id }); await reload(); rebuild(); }
843
+ catch (e) { un.disabled = false; alert(t("err.generic", { msg: e.message })); }
844
+ });
845
+ actions.appendChild(un);
846
+ card.appendChild(actions);
847
+ return card;
848
+ }
849
+
850
+ function localCard(ext) {
851
+ const cloudEntry = cloud.find((c) => c.id === ext.id);
852
+ const published = !!cloudEntry;
853
+ const card = el("div", { class: "studio-skill-card" });
854
+ const head = el("div", { class: "studio-skill-head" });
855
+ head.appendChild(el("span", { class: "studio-skill-name", text: `${ext.name} (${ext.id})` }));
856
+ head.appendChild(badge(published ? t("extlist.badge.published") : t("extlist.badge.local"), published ? "published" : "local"));
857
+ if (ext.error_count) head.appendChild(badge("✕ " + t("extlist.verify.errors", { n: ext.error_count }), "changed"));
858
+ else if (ext.warning_count) head.appendChild(badge("● " + t("extlist.verify.warnings", { n: ext.warning_count }), "changed"));
859
+ else head.appendChild(badge("✓ " + t("extlist.verify.ok"), "published"));
860
+ card.appendChild(head);
861
+ if (ext.description) card.appendChild(el("p", { class: "studio-skill-desc", text: ext.description }));
862
+
863
+ const meta = el("div", { class: "studio-skill-meta" });
864
+ if (ext.version) meta.appendChild(el("span", { text: "v" + ext.version }));
865
+ if (ext.units && ext.units.length) meta.appendChild(el("span", { text: ext.units.map((u) => u.kind).join(" · ") }));
866
+ card.appendChild(meta);
867
+
868
+ const actions = el("div", { class: "studio-actions" });
869
+ const pub = el("button", { class: "studio-btn studio-btn-primary", text: published ? t("extlist.btn.update") : t("extlist.btn.publish") });
870
+ pub.disabled = !licensed() || !!ext.error_count;
871
+ pub.title = licensed() ? (ext.error_count ? t("extlist.verify.errors", { n: ext.error_count }) : "") : t("extlist.needLicense");
872
+ pub.addEventListener("click", () => doPublish(ext, cloudEntry ? cloudEntry.version : null));
873
+ actions.appendChild(pub);
874
+
875
+ 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);
878
+ return card;
879
+ }
880
+
881
+ async function doPack(ext) {
882
+ try {
883
+ const data = await postJson("/pack", { ext_id: ext.id });
884
+ alert(t("extlist.packed", { path: data.path }));
885
+ } catch (e) {
886
+ alert(t("err.generic", { msg: e.message }));
887
+ }
888
+ }
889
+
890
+ async function doPublish(ext, prevVersion) {
891
+ const ok = await runPublishFlow(ext, prevVersion);
892
+ if (ok) { await reload(); rebuild(); }
893
+ }
894
+
895
+ function section(titleKey, hintKey, items, cardFn, emptyKey) {
896
+ const box = el("div", { class: "studio-skill-section" });
897
+ const head = el("div", { class: "studio-skill-section-head" });
898
+ head.appendChild(el("span", { class: "studio-label", text: t(titleKey) }));
899
+ head.appendChild(el("span", { class: "studio-skill-hint", text: t(hintKey) }));
900
+ box.appendChild(head);
901
+ if (!items.length) {
902
+ box.appendChild(el("p", { class: "studio-empty", text: t(emptyKey) }));
903
+ } else {
904
+ items.forEach((s) => box.appendChild(cardFn(s)));
905
+ }
906
+ return box;
907
+ }
908
+
909
+ function rebuild() {
910
+ if (!container) return;
911
+ container.innerHTML = "";
912
+
913
+ const newBox = el("div", { class: "studio-skill-promo" });
914
+ newBox.appendChild(el("p", { class: "studio-skill-promo-text", text: t("extlist.newExt.label") }));
915
+ newBox.appendChild(el("p", { class: "studio-skill-hint", text: t("extlist.newExt.hint") }));
916
+ const newBar = el("div", { class: "studio-actions" });
917
+ newBar.appendChild(el("button", { class: "studio-btn studio-btn-primary", text: t("extlist.newExt.btn"), onclick: () => createExtension(t("extlist.newExt.seed")) }));
918
+ newBox.appendChild(newBar);
919
+ container.appendChild(newBox);
920
+
921
+ container.appendChild(section("extlist.section.cloud", "extlist.section.cloudHint", cloud, cloudCard, "extlist.cloud.empty"));
922
+ container.appendChild(section("extlist.section.local", "extlist.section.localHint", store.extensions, localCard, "extlist.local.empty"));
923
+ }
924
+
925
+ return {
926
+ async attach(root) {
927
+ container = el("div", { class: "studio-panel" });
928
+ root.appendChild(container);
929
+ container.appendChild(el("p", { class: "studio-hint", text: t("extlist.loading") }));
930
+ try { await reload(); rebuild(); }
931
+ catch (e) { container.innerHTML = ""; container.appendChild(el("p", { class: "studio-empty", text: t("err.generic", { msg: e.message }) })); }
932
+ },
933
+ };
934
+ }
935
+
936
+ // ── Skills tab ────────────────────────────────────────────────────────────
937
+ // Reuses host endpoints: GET /api/creator/skills (403 => locked / not a
938
+ // creator), POST /api/my-skills/:name/publish. "Create / iterate" opens a
939
+ // new session via Sessions.startWith with the /skill-creator command.
940
+ function createSkillsPanel() {
941
+ let container = null;
942
+ let cloud = [];
943
+ let local = [];
944
+ let locked = false;
945
+
946
+ async function reload() {
947
+ const r = await getHost("/api/creator/skills");
948
+ if (!r.ok) throw new Error(r.data.error || `Request failed (${r.status})`);
949
+ locked = r.data.licensed === false;
950
+ cloud = r.data.cloud_skills || [];
951
+ local = r.data.local_skills || [];
952
+ }
953
+
954
+ async function publish(name, force) {
955
+ const url = `/api/my-skills/${encodeURIComponent(name)}/publish${force ? "?force=true" : ""}`;
956
+ const res = await fetch(url, { method: "POST" });
957
+ let data = {};
958
+ try { data = await res.json(); } catch (_e) {}
959
+ return { ok: res.ok && !!data.ok, already_exists: !!data.already_exists, error: data.error || null };
960
+ }
961
+
962
+ function createSkill(skill) {
963
+ const command = skill ? `/skill-creator ${t("skills.iterate.prompt")}${skill}` : "/skill-creator";
964
+ Sessions.startWith(command, { source: "manual" })
965
+ .catch((e) => alert(t("err.generic", { msg: e.message })));
966
+ }
967
+
968
+ function badge(text, kind) {
969
+ return el("span", { class: "studio-skill-badge studio-skill-badge-" + kind, text });
970
+ }
971
+
972
+ function cloudCard(skill) {
973
+ const card = el("div", { class: "studio-skill-card" });
974
+ const head = el("div", { class: "studio-skill-head" });
975
+ head.appendChild(el("span", { class: "studio-skill-name", text: skill.name }));
976
+ head.appendChild(badge(t("skills.badge.published"), "published"));
977
+ if (skill.has_local_changes) head.appendChild(badge("● " + t("skills.changed"), "changed"));
978
+ card.appendChild(head);
979
+ if (skill.description) card.appendChild(el("p", { class: "studio-skill-desc", text: skill.description }));
980
+
981
+ const meta = el("div", { class: "studio-skill-meta" });
982
+ if (skill.version) meta.appendChild(el("span", { text: "v" + skill.version }));
983
+ if (typeof skill.download_count === "number") meta.appendChild(el("span", { text: t("skills.downloads") + ": " + skill.download_count }));
984
+ card.appendChild(meta);
985
+
986
+ const actions = el("div", { class: "studio-actions" });
987
+ if (skill.local_present && skill.has_local_changes) {
988
+ const btn = el("button", { class: "studio-btn studio-btn-primary", text: t("skills.btn.update") });
989
+ btn.disabled = locked;
990
+ btn.title = locked ? t("skills.locked") : "";
991
+ btn.addEventListener("click", () => doPublish(skill.name, btn, true));
992
+ actions.appendChild(btn);
993
+ } else if (skill.local_present) {
994
+ const up = el("button", { class: "studio-btn", text: t("skills.btn.upToDate") });
995
+ up.disabled = true;
996
+ actions.appendChild(up);
997
+ }
998
+ if (skill.local_present) {
999
+ actions.appendChild(el("button", { class: "studio-btn", text: t("skills.btn.iterate"), onclick: () => createSkill(skill.name) }));
1000
+ }
1001
+ card.appendChild(actions);
1002
+ return card;
1003
+ }
1004
+
1005
+ function localCard(skill) {
1006
+ const card = el("div", { class: "studio-skill-card" });
1007
+ const head = el("div", { class: "studio-skill-head" });
1008
+ head.appendChild(el("span", { class: "studio-skill-name", text: skill.name }));
1009
+ head.appendChild(badge(t("skills.badge.unpublished"), "local"));
1010
+ if (skill.shadowing_brand) head.appendChild(badge("⚡ " + t("skills.shadow.label"), "shadow"));
1011
+ card.appendChild(head);
1012
+ if (skill.description) card.appendChild(el("p", { class: "studio-skill-desc", text: skill.description }));
1013
+
1014
+ const actions = el("div", { class: "studio-actions" });
1015
+ const btn = el("button", { class: "studio-btn studio-btn-primary", text: t("skills.btn.publish") });
1016
+ btn.disabled = locked;
1017
+ btn.title = locked ? t("skills.locked") : "";
1018
+ btn.addEventListener("click", () => doPublish(skill.name, btn, false));
1019
+ actions.appendChild(btn);
1020
+ card.appendChild(actions);
1021
+ return card;
1022
+ }
1023
+
1024
+ async function doPublish(name, btn, isUpdate) {
1025
+ if (btn.disabled) return;
1026
+ btn.disabled = true;
1027
+ const label = btn.textContent;
1028
+ btn.textContent = t("skills.publishing");
1029
+ try {
1030
+ let result = await publish(name, isUpdate);
1031
+ if (!result.ok && result.already_exists && !isUpdate) {
1032
+ if (window.confirm(`"${name}" already exists on the platform. Overwrite?`)) {
1033
+ result = await publish(name, true);
1034
+ } else {
1035
+ btn.disabled = false;
1036
+ btn.textContent = label;
1037
+ return;
1038
+ }
1039
+ }
1040
+ if (!result.ok) throw new Error(result.error || "Publish failed");
1041
+ btn.textContent = "✓";
1042
+ await reload();
1043
+ rebuild();
1044
+ } catch (e) {
1045
+ btn.disabled = false;
1046
+ btn.textContent = label;
1047
+ alert(t("err.generic", { msg: e.message }));
1048
+ }
1049
+ }
1050
+
1051
+ function section(titleKey, hintKey, items, cardFn, emptyKey) {
1052
+ const box = el("div", { class: "studio-skill-section" });
1053
+ const head = el("div", { class: "studio-skill-section-head" });
1054
+ head.appendChild(el("span", { class: "studio-label", text: t(titleKey) }));
1055
+ head.appendChild(el("span", { class: "studio-skill-hint", text: t(hintKey) }));
1056
+ box.appendChild(head);
1057
+ if (!items.length) {
1058
+ box.appendChild(el("p", { class: "studio-empty", text: t(emptyKey) }));
1059
+ } else {
1060
+ items.forEach((s) => box.appendChild(cardFn(s)));
1061
+ }
1062
+ return box;
1063
+ }
1064
+
1065
+ function rebuild() {
1066
+ if (!container) return;
1067
+ container.innerHTML = "";
1068
+
1069
+ if (locked) {
1070
+ const promo = el("div", { class: "studio-skill-promo" });
1071
+ promo.appendChild(el("p", { class: "studio-skill-promo-text", text: t("skills.promo.text") }));
1072
+ promo.appendChild(el("p", { class: "studio-empty", text: t("skills.locked") }));
1073
+ container.appendChild(promo);
1074
+ }
1075
+
1076
+ const newBox = el("div", { class: "studio-skill-promo" });
1077
+ newBox.appendChild(el("p", { class: "studio-skill-promo-text", text: t("skills.newSkill.btn") }));
1078
+ newBox.appendChild(el("p", { class: "studio-skill-hint", text: t("skills.newSkill.label") }));
1079
+ const newBar = el("div", { class: "studio-actions" });
1080
+ newBar.appendChild(el("button", { class: "studio-btn studio-btn-primary", text: t("skills.newSkill.btn"), onclick: () => createSkill(null) }));
1081
+ newBox.appendChild(newBar);
1082
+ container.appendChild(newBox);
1083
+
1084
+ const cloudEmptyKey = locked ? "skills.cloud.locked" : "skills.cloud.empty";
1085
+ container.appendChild(section("skills.section.cloud", "skills.section.cloudHint", cloud, cloudCard, cloudEmptyKey));
1086
+ container.appendChild(section("skills.section.local", "skills.section.localHint", local, localCard, "skills.local.empty"));
1087
+ }
1088
+
1089
+ return {
1090
+ async attach(root) {
1091
+ container = el("div", { class: "studio-panel" });
1092
+ root.appendChild(container);
1093
+ container.appendChild(el("p", { class: "studio-hint", text: t("skills.loading") }));
1094
+ try { await reload(); rebuild(); }
1095
+ catch (e) { container.innerHTML = ""; container.appendChild(el("p", { class: "studio-empty", text: t("err.generic", { msg: e.message }) })); }
1096
+ },
1097
+ };
1098
+ }
1099
+
1100
+ // ── Full-page workspace: top tabs (Extensions / Skills) ────────────────────
1101
+ const WS_ID = "ext-studio";
1102
+
1103
+ function renderWorkspace(root) {
1104
+ root.innerHTML = "";
1105
+ const page = el("div", { class: "studio-page" });
1106
+ root.appendChild(page);
1107
+
1108
+ const header = el("div", { class: "studio-page-head" });
1109
+ header.appendChild(el("h2", { class: "studio-page-title", text: t("ws.title") }));
1110
+ page.appendChild(header);
1111
+
1112
+ const tabsBar = el("div", { class: "studio-tabs" });
1113
+ const body = el("div", { class: "studio-tab-body" });
1114
+ page.appendChild(tabsBar);
1115
+ page.appendChild(body);
1116
+
1117
+ const tabs = [
1118
+ { id: "extensions", label: t("tab.extensions"), build: buildExtensionsTab },
1119
+ { id: "skills", label: t("tab.skills"), build: buildSkillsTab },
1120
+ ];
1121
+ let active = null;
1122
+
1123
+ function select(id) {
1124
+ active = id;
1125
+ Array.from(tabsBar.children).forEach((b) => b.classList.toggle("studio-tab-active", b.dataset.tab === id));
1126
+ body.innerHTML = "";
1127
+ const tab = tabs.find((t2) => t2.id === id);
1128
+ if (tab) tab.build(body);
1129
+ }
1130
+
1131
+ tabs.forEach((tab) => {
1132
+ const btn = el("button", { class: "studio-tab", text: tab.label });
1133
+ btn.dataset.tab = tab.id;
1134
+ btn.addEventListener("click", () => select(tab.id));
1135
+ tabsBar.appendChild(btn);
1136
+ });
1137
+
1138
+ select("extensions");
1139
+ }
1140
+
1141
+ function buildExtensionsTab(root) {
1142
+ createExtensionsPanel().attach(root);
1143
+ }
1144
+
1145
+ function buildSkillsTab(root) {
1146
+ createSkillsPanel().attach(root);
1147
+ }
1148
+
1149
+ function navRow(label, onClick) {
1150
+ const item = el("div", { class: "task-item task-item-summary" });
1151
+ item.innerHTML =
1152
+ '<div class="task-row">' +
1153
+ '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" ' +
1154
+ 'fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" ' +
1155
+ 'stroke-linejoin="round" class="task-icon">' +
1156
+ '<path d="M12 2L2 7l10 5 10-5-10-5z"/>' +
1157
+ '<path d="M2 17l10 5 10-5"/>' +
1158
+ '<path d="M2 12l10 5 10-5"/>' +
1159
+ '</svg>' +
1160
+ '<div class="task-info"><span class="task-name"></span></div>' +
1161
+ '</div>';
1162
+ item.querySelector(".task-name").textContent = label;
1163
+ item.addEventListener("click", onClick);
1164
+ return item;
1165
+ }
1166
+
1167
+ Clacky.ext.ui.registerWorkspace(WS_ID, {
1168
+ title: t("ws.title"),
1169
+ render(container) { renderWorkspace(container); },
1170
+ });
1171
+
1172
+ Clacky.ext.ui.mount("sidebar.nav.bottom", function () {
1173
+ return navRow(t("nav.entry"), function () { Clacky.ext.ui.openWorkspace(WS_ID); });
1174
+ }, { workspace: WS_ID });
1175
+
1176
+ // Extension-developer session tools: debug + publish, as aside tabs. These
1177
+ // are session-scoped (the panel is bound to the ext-developer profile in
1178
+ // ext.yml), so they only appear inside a dev session's aside, not the
1179
+ // full-page workspace.
1180
+ Clacky.ext.ui.mount("session.aside", {
1181
+ render(container) { createDebugPanel().attach(container); },
1182
+ }, { tab: { id: "ext-debug", label: () => t("ext.debug.section") }, order: 10 });
1183
+
1184
+ Clacky.ext.ui.mount("session.aside", {
1185
+ render(container) { createPublishPanel().attach(container); },
1186
+ }, { tab: { id: "ext-publish", label: () => t("ext.publish.section") }, order: 20 });
1187
+
1188
+ const style = document.createElement("style");
1189
+ style.textContent = `
1190
+ .studio-panel { padding: 16px; font-size: 13px; color: var(--color-text-secondary); }
1191
+ .studio-page .studio-panel { padding: 0; }
1192
+ .studio-field { margin-bottom: 14px; }
1193
+ .studio-label { display: block; font-size: 12px; font-weight: 600; color: var(--color-text-tertiary); margin-bottom: 6px; }
1194
+ .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
+ .studio-select:focus, .studio-textarea:focus { border-color: var(--color-accent-primary); outline: none; }
1196
+ .studio-empty { color: var(--color-text-muted); font-size: 12px; margin: 4px 0; }
1197
+ .studio-hint { color: var(--color-text-tertiary); font-size: 12px; line-height: 1.5; margin: 10px 0 0; }
1198
+ .studio-detail { border: 1px solid var(--color-border-primary); border-radius: var(--radius-sm); padding: 12px; margin-bottom: 14px; background: var(--color-bg-secondary); }
1199
+ .studio-detail-name { margin: 0 0 4px; font-size: 14px; color: var(--color-text-primary); }
1200
+ .studio-detail-desc { margin: 0 0 10px; font-size: 12px; color: var(--color-text-secondary); line-height: 1.5; }
1201
+ .studio-meta { display: grid; grid-template-columns: auto 1fr; gap: 4px 12px; margin: 0 0 10px; font-size: 12px; }
1202
+ .studio-meta dt { color: var(--color-text-tertiary); }
1203
+ .studio-meta dd { margin: 0; color: var(--color-text-primary); }
1204
+ .studio-units { display: flex; flex-wrap: wrap; gap: 6px; }
1205
+ .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; }
1207
+ .studio-skill-promo .studio-actions { margin: 12px 0 0; }
1208
+ .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
+ .studio-btn:hover { background: var(--color-bg-hover); color: var(--color-text-primary); }
1210
+ .studio-btn-primary { background: var(--color-button-primary); color: var(--color-button-primary-text); border-color: transparent; }
1211
+ .studio-btn-primary:hover { background: var(--color-button-primary-hover); color: var(--color-button-primary-text); }
1212
+ .studio-btn-primary:disabled { opacity: 0.6; cursor: default; }
1213
+ .studio-btn-danger { color: var(--color-error); border-color: var(--color-error-border); padding: 4px 10px; }
1214
+ .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); }
1217
+ .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; }
1220
+ .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
+ .studio-issue-error { border-left-color: var(--color-error); }
1222
+ .studio-issue-warning { border-left-color: var(--color-warning, var(--color-text-tertiary)); }
1223
+ .studio-issue-code { font-size: 11px; font-family: monospace; color: var(--color-text-tertiary); margin-bottom: 2px; }
1224
+ .studio-issue-msg { font-size: 12px; color: var(--color-text-primary); line-height: 1.4; }
1225
+ .studio-issue-file { font-size: 11px; font-family: monospace; color: var(--color-text-muted); margin-top: 2px; }
1226
+ .studio-issue-hint { font-size: 11px; color: var(--color-text-secondary); margin-top: 4px; font-style: italic; }
1227
+ .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; }
1229
+ .studio-feedback-success { color: var(--color-success); }
1230
+ .studio-feedback-error { color: var(--color-error); }
1231
+ .studio-feedback-warn { color: var(--color-warning, var(--color-text-secondary)); }
1232
+ .studio-published { margin-top: 16px; border-top: 1px solid var(--color-border-primary); padding-top: 12px; }
1233
+ .studio-published-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 6px 0; font-size: 12px; }
1234
+ .studio-published-name { color: var(--color-text-primary); }
1235
+ .studio-page { width: 100%; }
1236
+ .studio-page-head { margin-bottom: 20px; }
1237
+ .studio-page-title { margin: 0; font-size: 22px; font-weight: 600; color: var(--color-text-primary); }
1238
+ .studio-tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--color-border-primary); margin-bottom: 24px; }
1239
+ .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; }
1240
+ .studio-tab:hover { color: var(--color-text-primary); }
1241
+ .studio-tab-active { color: var(--color-text-primary); border-bottom-color: var(--color-accent-primary); }
1242
+ .studio-ext-block { margin-bottom: 20px; }
1243
+ .studio-block-title { font-size: 13px; font-weight: 600; color: var(--color-text-secondary); padding: 8px 0 0; }
1244
+ .studio-skill-section { margin-bottom: 28px; }
1245
+ .studio-skill-section-head { display: flex; align-items: baseline; gap: 10px; margin-bottom: 12px; }
1246
+ .studio-skill-hint { font-size: 11px; color: var(--color-text-muted); }
1247
+ .studio-skill-card { border: 1px solid var(--color-border-primary); border-radius: var(--radius-md, 8px); padding: 14px 16px; margin: 0 0 10px; background: var(--color-bg-secondary); }
1248
+ .studio-skill-head { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
1249
+ .studio-skill-name { font-size: 14px; font-weight: 600; color: var(--color-text-primary); }
1250
+ .studio-skill-badge { font-size: 11px; padding: 1px 8px; border-radius: 10px; }
1251
+ .studio-skill-badge-published { background: var(--color-success-bg, var(--color-bg-hover)); color: var(--color-success); }
1252
+ .studio-skill-badge-local { background: var(--color-bg-hover); color: var(--color-text-tertiary); }
1253
+ .studio-skill-badge-changed { background: var(--color-warning-bg, var(--color-bg-hover)); color: var(--color-warning, var(--color-text-secondary)); }
1254
+ .studio-skill-badge-shadow { background: var(--color-bg-hover); color: var(--color-text-secondary); }
1255
+ .studio-skill-desc { margin: 6px 0 8px; font-size: 12px; color: var(--color-text-secondary); line-height: 1.5; }
1256
+ .studio-skill-meta { display: flex; gap: 12px; font-size: 11px; color: var(--color-text-muted); margin-bottom: 8px; }
1257
+ .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
+ .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); }
1262
+ .studio-modal-body { font-size: 13px; color: var(--color-text-secondary); line-height: 1.6; }
1263
+ .studio-modal-status { margin: 0 0 8px; font-size: 13px; color: var(--color-text-secondary); line-height: 1.6; }
1264
+ .studio-modal-code { margin: 0 0 8px; font-size: 13px; font-family: monospace; color: var(--color-text-primary); }
1265
+ .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; }
1267
+ `;
1268
+ document.head.appendChild(style);
1269
+ })();