aimeat 1.16.2 → 1.18.0

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 (281) hide show
  1. package/dist/.env.example +6 -1
  2. package/dist/locales/en.json +166 -4
  3. package/dist/locales/fi.json +166 -4
  4. package/dist/public/components/Card.js +17 -3
  5. package/dist/public/components/Collapsible.js +39 -0
  6. package/dist/public/components/CopyButton.js +55 -24
  7. package/dist/public/components/DataTable.js +57 -0
  8. package/dist/public/components/EmptyState.js +28 -0
  9. package/dist/public/components/KeyValueRow.js +37 -0
  10. package/dist/public/components/Modal.js +22 -5
  11. package/dist/public/components/Pagination.js +70 -0
  12. package/dist/public/components/SearchBar.js +44 -0
  13. package/dist/public/components/StatusDot.js +29 -0
  14. package/dist/public/components/TagList.js +53 -0
  15. package/dist/public/components/ToggleSwitch.js +28 -0
  16. package/dist/public/components/index.js +41 -1
  17. package/dist/public/css/components/tags.css +24 -0
  18. package/dist/public/css/style-testbed.css +135 -0
  19. package/dist/public/css/theme.css +556 -34
  20. package/dist/public/css/views/admin-agent-integration.css +2 -33
  21. package/dist/public/css/views/admin.css +730 -686
  22. package/dist/public/css/views/agents-detail.css +402 -14
  23. package/dist/public/css/views/help.css +2 -1
  24. package/dist/public/css/views/hobbies.css +53 -33
  25. package/dist/public/css/views/marketplace.css +73 -60
  26. package/dist/public/css/views/openclaw.css +12 -6
  27. package/dist/public/css/views/portal-classic.css +34 -26
  28. package/dist/public/css/views/portal-dev.css +41 -32
  29. package/dist/public/css/views/portal.css +14 -18
  30. package/dist/public/css/views/profile.css +252 -164
  31. package/dist/public/css/views/public-knowledge-viewer.css +18 -6
  32. package/dist/public/css/views/scheduler.css +100 -0
  33. package/dist/public/js/format.js +41 -0
  34. package/dist/public/js/services/agent-tasks.js +14 -1
  35. package/dist/public/js/services/generator-spec-validate.js +5 -1
  36. package/dist/public/js/services/schedules.js +53 -0
  37. package/dist/public/js/style-testbed.js +170 -0
  38. package/dist/public/js/theme.js +114 -0
  39. package/dist/public/spa.html +74 -16
  40. package/dist/public/style-testbed.html +420 -0
  41. package/dist/public/views/admin/agent-integration-tab.js +23 -38
  42. package/dist/public/views/admin/agent-tasks-tab.js +14 -21
  43. package/dist/public/views/admin/chat-instances-tab.js +1 -1
  44. package/dist/public/views/admin/config-tab.js +7 -2
  45. package/dist/public/views/admin/cortex-tab.js +20 -14
  46. package/dist/public/views/admin/csm-tab.js +22 -18
  47. package/dist/public/views/admin/directory-tab.js +3 -3
  48. package/dist/public/views/admin/email-tab.js +23 -28
  49. package/dist/public/views/admin/federation-tab.js +15 -5
  50. package/dist/public/views/admin/generator-debug-tab.js +10 -8
  51. package/dist/public/views/admin/genesis-tab.js +4 -4
  52. package/dist/public/views/admin/ghii-tab.js +3 -3
  53. package/dist/public/views/admin/knowledge-tab.js +15 -5
  54. package/dist/public/views/admin/matching-tab.js +3 -3
  55. package/dist/public/views/admin/memory-tab.js +16 -20
  56. package/dist/public/views/admin/msm-tab.js +30 -17
  57. package/dist/public/views/admin/overview-tab.js +7 -7
  58. package/dist/public/views/admin/packages-tab.js +10 -9
  59. package/dist/public/views/admin/portal-tab.js +14 -4
  60. package/dist/public/views/admin/prompts-tab.js +5 -5
  61. package/dist/public/views/admin/push-tab.js +15 -4
  62. package/dist/public/views/admin/realtime-tab.js +3 -3
  63. package/dist/public/views/admin/services-tab.js +18 -27
  64. package/dist/public/views/admin/shared.js +48 -58
  65. package/dist/public/views/admin/sharing-groups-tab.js +5 -10
  66. package/dist/public/views/admin/stats-tab.js +3 -3
  67. package/dist/public/views/guides.js +9 -13
  68. package/dist/public/views/help.js +3 -24
  69. package/dist/public/views/hobbies.js +47 -41
  70. package/dist/public/views/marketplace.js +42 -34
  71. package/dist/public/views/openclaw.js +0 -1
  72. package/dist/public/views/portal-classic.js +4 -15
  73. package/dist/public/views/portal-dev.js +28 -30
  74. package/dist/public/views/portal.js +16 -21
  75. package/dist/public/views/portfolio.js +11 -12
  76. package/dist/public/views/profile/access-tab.js +250 -1
  77. package/dist/public/views/profile/agents/agent-card.js +75 -4
  78. package/dist/public/views/profile/agents/tab-activity.js +15 -13
  79. package/dist/public/views/profile/agents/tab-agent-config.js +60 -6
  80. package/dist/public/views/profile/agents/tab-integration.js +37 -60
  81. package/dist/public/views/profile/agents/tab-messages.js +10 -1
  82. package/dist/public/views/profile/agents/tab-quality.js +25 -19
  83. package/dist/public/views/profile/agents/tab-schedules.js +110 -0
  84. package/dist/public/views/profile/agents-activity-subtab.js +4 -4
  85. package/dist/public/views/profile/agents-services-subtab.js +5 -4
  86. package/dist/public/views/profile/agents-tab.js +141 -37
  87. package/dist/public/views/profile/agents-tasks-subtab.js +221 -26
  88. package/dist/public/views/profile/calibrator-batch.js +5 -11
  89. package/dist/public/views/profile/calibrator-tab.js +4 -9
  90. package/dist/public/views/profile/chat-sessions-tab.js +9 -5
  91. package/dist/public/views/profile/email-tab.js +2 -2
  92. package/dist/public/views/profile/extensions-tab.js +27 -22
  93. package/dist/public/views/profile/federation-tab.js +4 -1
  94. package/dist/public/views/profile/foundry-detail.js +22 -23
  95. package/dist/public/views/profile/foundry-tab.js +12 -13
  96. package/dist/public/views/profile/generator-dashboard/DebugPanel.js +4 -7
  97. package/dist/public/views/profile/generator-dashboard/use-dashboard-core.js +4 -1
  98. package/dist/public/views/profile/generator-dashboard/use-edit-mode.js +3 -2
  99. package/dist/public/views/profile/generator-detail.js +26 -30
  100. package/dist/public/views/profile/generator-tab.js +5 -5
  101. package/dist/public/views/profile/knowledge-tab.js +12 -12
  102. package/dist/public/views/profile/landing-page.js +100 -149
  103. package/dist/public/views/profile/memory-tab.js +15 -11
  104. package/dist/public/views/profile/node-stats-tab.js +3 -3
  105. package/dist/public/views/profile/nodes-tab.js +10 -3
  106. package/dist/public/views/profile/notifications-tab.js +3 -3
  107. package/dist/public/views/profile/packages-tab.js +14 -14
  108. package/dist/public/views/profile/scheduler-tab.js +340 -0
  109. package/dist/public/views/profile/services-tab.js +1 -1
  110. package/dist/public/views/profile/shared.js +16 -12
  111. package/dist/public/views/profile/wallet-tab.js +27 -50
  112. package/dist/public/views/profile/work-tab.js +29 -31
  113. package/dist/public/views/profile.js +24 -0
  114. package/dist/public/views/public-knowledge-viewer.js +9 -18
  115. package/dist/src/auth/middleware.d.ts +3 -2
  116. package/dist/src/auth/middleware.d.ts.map +1 -1
  117. package/dist/src/auth/middleware.js +78 -3
  118. package/dist/src/auth/middleware.js.map +1 -1
  119. package/dist/src/cli/init-wizard.d.ts.map +1 -1
  120. package/dist/src/cli/init-wizard.js +13 -1
  121. package/dist/src/cli/init-wizard.js.map +1 -1
  122. package/dist/src/config.d.ts +6 -0
  123. package/dist/src/config.d.ts.map +1 -1
  124. package/dist/src/config.js +12 -1
  125. package/dist/src/config.js.map +1 -1
  126. package/dist/src/generated/api-types.d.ts +708 -8
  127. package/dist/src/generated/api-types.d.ts.map +1 -1
  128. package/dist/src/mcp/agent-schedules.d.ts +23 -0
  129. package/dist/src/mcp/agent-schedules.d.ts.map +1 -0
  130. package/dist/src/mcp/agent-schedules.js +175 -0
  131. package/dist/src/mcp/agent-schedules.js.map +1 -0
  132. package/dist/src/mcp/annotations.d.ts.map +1 -1
  133. package/dist/src/mcp/annotations.js +6 -0
  134. package/dist/src/mcp/annotations.js.map +1 -1
  135. package/dist/src/mcp/catalog/definitions.d.ts.map +1 -1
  136. package/dist/src/mcp/catalog/definitions.js +55 -0
  137. package/dist/src/mcp/catalog/definitions.js.map +1 -1
  138. package/dist/src/mcp/catalog/scopes.d.ts.map +1 -1
  139. package/dist/src/mcp/catalog/scopes.js +12 -0
  140. package/dist/src/mcp/catalog/scopes.js.map +1 -1
  141. package/dist/src/mcp/catalog/surfaces.d.ts.map +1 -1
  142. package/dist/src/mcp/catalog/surfaces.js +2 -0
  143. package/dist/src/mcp/catalog/surfaces.js.map +1 -1
  144. package/dist/src/mcp/index.d.ts.map +1 -1
  145. package/dist/src/mcp/index.js +2 -0
  146. package/dist/src/mcp/index.js.map +1 -1
  147. package/dist/src/models/agent-task-schemas.d.ts +6 -0
  148. package/dist/src/models/agent-task-schemas.d.ts.map +1 -1
  149. package/dist/src/models/agent-task-schemas.js +6 -0
  150. package/dist/src/models/agent-task-schemas.js.map +1 -1
  151. package/dist/src/models/schemas.d.ts +1 -0
  152. package/dist/src/models/schemas.d.ts.map +1 -1
  153. package/dist/src/models/schemas.js +4 -0
  154. package/dist/src/models/schemas.js.map +1 -1
  155. package/dist/src/routes/access-tokens.d.ts +17 -0
  156. package/dist/src/routes/access-tokens.d.ts.map +1 -0
  157. package/dist/src/routes/access-tokens.js +164 -0
  158. package/dist/src/routes/access-tokens.js.map +1 -0
  159. package/dist/src/routes/agent-tasks.d.ts +2 -0
  160. package/dist/src/routes/agent-tasks.d.ts.map +1 -1
  161. package/dist/src/routes/agent-tasks.js +118 -23
  162. package/dist/src/routes/agent-tasks.js.map +1 -1
  163. package/dist/src/routes/agents.d.ts.map +1 -1
  164. package/dist/src/routes/agents.js +46 -0
  165. package/dist/src/routes/agents.js.map +1 -1
  166. package/dist/src/routes/ai.d.ts +6 -9
  167. package/dist/src/routes/ai.d.ts.map +1 -1
  168. package/dist/src/routes/ai.js +26 -175
  169. package/dist/src/routes/ai.js.map +1 -1
  170. package/dist/src/routes/apps.d.ts.map +1 -1
  171. package/dist/src/routes/apps.js +104 -34
  172. package/dist/src/routes/apps.js.map +1 -1
  173. package/dist/src/routes/auth.d.ts.map +1 -1
  174. package/dist/src/routes/auth.js +77 -4
  175. package/dist/src/routes/auth.js.map +1 -1
  176. package/dist/src/routes/cortex.d.ts +2 -1
  177. package/dist/src/routes/cortex.d.ts.map +1 -1
  178. package/dist/src/routes/cortex.js +13 -8
  179. package/dist/src/routes/cortex.js.map +1 -1
  180. package/dist/src/routes/extensions.d.ts.map +1 -1
  181. package/dist/src/routes/extensions.js +85 -51
  182. package/dist/src/routes/extensions.js.map +1 -1
  183. package/dist/src/routes/generator.d.ts.map +1 -1
  184. package/dist/src/routes/generator.js +109 -43
  185. package/dist/src/routes/generator.js.map +1 -1
  186. package/dist/src/routes/ghii.d.ts.map +1 -1
  187. package/dist/src/routes/ghii.js +34 -13
  188. package/dist/src/routes/ghii.js.map +1 -1
  189. package/dist/src/routes/libs.d.ts +15 -0
  190. package/dist/src/routes/libs.d.ts.map +1 -1
  191. package/dist/src/routes/libs.js +178 -55
  192. package/dist/src/routes/libs.js.map +1 -1
  193. package/dist/src/routes/portal.d.ts.map +1 -1
  194. package/dist/src/routes/portal.js +1 -0
  195. package/dist/src/routes/portal.js.map +1 -1
  196. package/dist/src/routes/schedules.d.ts +33 -0
  197. package/dist/src/routes/schedules.d.ts.map +1 -0
  198. package/dist/src/routes/schedules.js +380 -0
  199. package/dist/src/routes/schedules.js.map +1 -0
  200. package/dist/src/server-bootstrap/config-init.js +2 -2
  201. package/dist/src/server-bootstrap/config-init.js.map +1 -1
  202. package/dist/src/server-bootstrap/routes-loader.d.ts.map +1 -1
  203. package/dist/src/server-bootstrap/routes-loader.js +7 -0
  204. package/dist/src/server-bootstrap/routes-loader.js.map +1 -1
  205. package/dist/src/server-bootstrap/service-init.d.ts.map +1 -1
  206. package/dist/src/server-bootstrap/service-init.js +2 -1
  207. package/dist/src/server-bootstrap/service-init.js.map +1 -1
  208. package/dist/src/services/access-token.d.ts +28 -0
  209. package/dist/src/services/access-token.d.ts.map +1 -0
  210. package/dist/src/services/access-token.js +37 -0
  211. package/dist/src/services/access-token.js.map +1 -0
  212. package/dist/src/services/ai-completion.d.ts +83 -0
  213. package/dist/src/services/ai-completion.d.ts.map +1 -0
  214. package/dist/src/services/ai-completion.js +187 -0
  215. package/dist/src/services/ai-completion.js.map +1 -0
  216. package/dist/src/services/config-schema.d.ts.map +1 -1
  217. package/dist/src/services/config-schema.js +3 -0
  218. package/dist/src/services/config-schema.js.map +1 -1
  219. package/dist/src/services/generator-prompt-seeds.d.ts.map +1 -1
  220. package/dist/src/services/generator-prompt-seeds.js +6 -3
  221. package/dist/src/services/generator-prompt-seeds.js.map +1 -1
  222. package/dist/src/services/generator-prompts/spec-validate.d.ts.map +1 -1
  223. package/dist/src/services/generator-prompts/spec-validate.js +6 -2
  224. package/dist/src/services/generator-prompts/spec-validate.js.map +1 -1
  225. package/dist/src/services/generator-registration.d.ts +12 -4
  226. package/dist/src/services/generator-registration.d.ts.map +1 -1
  227. package/dist/src/services/generator-registration.js +71 -0
  228. package/dist/src/services/generator-registration.js.map +1 -1
  229. package/dist/src/services/generator-validate.d.ts +10 -0
  230. package/dist/src/services/generator-validate.d.ts.map +1 -1
  231. package/dist/src/services/generator-validate.js +180 -36
  232. package/dist/src/services/generator-validate.js.map +1 -1
  233. package/dist/src/services/owner-session.d.ts +43 -0
  234. package/dist/src/services/owner-session.d.ts.map +1 -0
  235. package/dist/src/services/owner-session.js +175 -0
  236. package/dist/src/services/owner-session.js.map +1 -0
  237. package/dist/src/services/schedule-constraints.d.ts +65 -0
  238. package/dist/src/services/schedule-constraints.d.ts.map +1 -0
  239. package/dist/src/services/schedule-constraints.js +106 -0
  240. package/dist/src/services/schedule-constraints.js.map +1 -0
  241. package/dist/src/services/scheduler.d.ts +34 -0
  242. package/dist/src/services/scheduler.d.ts.map +1 -1
  243. package/dist/src/services/scheduler.js +266 -22
  244. package/dist/src/services/scheduler.js.map +1 -1
  245. package/dist/src/services/skill-bundle/generator.js +19 -0
  246. package/dist/src/services/skill-bundle/generator.js.map +1 -1
  247. package/dist/src/storage/interface.d.ts +48 -4
  248. package/dist/src/storage/interface.d.ts.map +1 -1
  249. package/dist/src/storage/interface.js.map +1 -1
  250. package/dist/src/storage/providers/mongodb/index.d.ts +31 -0
  251. package/dist/src/storage/providers/mongodb/index.d.ts.map +1 -1
  252. package/dist/src/storage/providers/mongodb/index.js +187 -9
  253. package/dist/src/storage/providers/mongodb/index.js.map +1 -1
  254. package/dist/src/storage/providers/sqlite/index.d.ts +31 -0
  255. package/dist/src/storage/providers/sqlite/index.d.ts.map +1 -1
  256. package/dist/src/storage/providers/sqlite/index.js +131 -8
  257. package/dist/src/storage/providers/sqlite/index.js.map +1 -1
  258. package/dist/src/storage/providers/sqlite/repos/agent-task.d.ts.map +1 -1
  259. package/dist/src/storage/providers/sqlite/repos/agent-task.js +6 -4
  260. package/dist/src/storage/providers/sqlite/repos/agent-task.js.map +1 -1
  261. package/dist/src/storage/providers/sqlite/schema.d.ts.map +1 -1
  262. package/dist/src/storage/providers/sqlite/schema.js +79 -8
  263. package/dist/src/storage/providers/sqlite/schema.js.map +1 -1
  264. package/dist/src/storage/repositories/pat.repository.d.ts +40 -0
  265. package/dist/src/storage/repositories/pat.repository.d.ts.map +1 -0
  266. package/dist/src/storage/repositories/pat.repository.js +14 -0
  267. package/dist/src/storage/repositories/pat.repository.js.map +1 -0
  268. package/dist/src/storage/repositories/scheduler.repository.d.ts +2 -0
  269. package/dist/src/storage/repositories/scheduler.repository.d.ts.map +1 -1
  270. package/dist/src/storage/repositories/session.repository.d.ts +51 -0
  271. package/dist/src/storage/repositories/session.repository.d.ts.map +1 -1
  272. package/dist/src/storage/repositories/session.repository.js +12 -0
  273. package/dist/src/storage/repositories/session.repository.js.map +1 -1
  274. package/dist/src/utils/env-config.d.ts.map +1 -1
  275. package/dist/src/utils/env-config.js +25 -1
  276. package/dist/src/utils/env-config.js.map +1 -1
  277. package/dist/src/utils/env-validator.d.ts.map +1 -1
  278. package/dist/src/utils/env-validator.js +4 -0
  279. package/dist/src/utils/env-validator.js.map +1 -1
  280. package/package.json +1 -1
  281. package/prisma/schema.prisma +48 -0
@@ -14,6 +14,15 @@
14
14
  * @version-history
15
15
  * v1.0.0 — 2026-03-10 — Initial implementation with Cortex + Server extension management
16
16
  * v1.1.0 — 2026-03-17 — Refactor: replace all inline style="" attributes with CSS classes
17
+ * v1.2.0 — 2026-06-02 — Component unification (#2): both install modals use the
18
+ * canonical <Modal> component (className="ext-modal-narrow" preserves width).
19
+ * v1.3.0 — 2026-06-02 — Component unification (#1): the five tier-2 clipboard
20
+ * buttons (copy prompt / script tag / API surface / instance ID / endpoint) now
21
+ * use the canonical <CopyButton> with onCopied toasts. The two "Create with AI"
22
+ * hero CTAs keep their labeled buttons + copyToClipboard (not copy buttons).
23
+ * v1.4.0 — 2026-06-02 — Component unification (#11): all six extension/instance
24
+ * status dots use the canonical <StatusDot> instead of bespoke .ext-status-dot
25
+ * (which had hardcoded #4ade80/#9ca3af — now tokenized via the component).
17
26
  */
18
27
  import { h } from 'preact';
19
28
  import { useState, useEffect } from 'preact/hooks';
@@ -22,7 +31,9 @@ const html = htm.bind(h);
22
31
  import { t } from '/js/i18n.js';
23
32
  import { escHtml, copyToClipboard } from '/js/utils.js';
24
33
  import { Spinner } from './shared.js';
25
- import { useConfirm } from '/components/Modal.js';
34
+ import { Modal, useConfirm } from '/components/Modal.js';
35
+ import { CopyButton } from '/components/CopyButton.js';
36
+ import { StatusDot } from '/components/StatusDot.js';
26
37
  import * as cortexService from '/js/services/cortex.js';
27
38
  import * as v8Ext from '/js/services/extensions.js';
28
39
  import { getNodeUrl, getSession } from '/js/services/auth.js';
@@ -648,7 +659,7 @@ export default function ExtensionsTab({ session, showToast }) {
648
659
  <div class="ext-meta-row">
649
660
  <span>${t('profile.extensions.detail.author')}: ${ext.author || '?'}</span>
650
661
  ${ext.license ? html`<span>${t('profile.extensions.detail.license')}: ${ext.license}</span>` : null}
651
- <span><span class="ext-status-dot ${ext.status}"></span> ${t('profile.extensions.status.' + ext.status)}</span>
662
+ <span><${StatusDot} status=${ext.status} /> ${t('profile.extensions.status.' + ext.status)}</span>
652
663
  <span>${t('profile.extensions.detail.tags')}: ${(ext.tags || []).join(', ')}</span>
653
664
  </div>
654
665
  </div>
@@ -662,7 +673,7 @@ export default function ExtensionsTab({ session, showToast }) {
662
673
  const content = p._content || p.content || '';
663
674
  return html`
664
675
  <div class="ext-detail-section">
665
- <div class="ext-detail-section-title">${'\u{1F4AC}'} Prompt: ${p.name} <button class="btn-primary btn-sm" onClick=${() => { copyToClipboard(content); showToast(t('profile.extensions.detail.copied')); }}>${t('profile.extensions.detail.copyPrompt')}</button></div>
676
+ <div class="ext-detail-section-title">${'\u{1F4AC}'} Prompt: ${p.name} <${CopyButton} text=${content} label=${t('profile.extensions.detail.copyPrompt')} className="btn-primary btn-sm" onCopied=${() => showToast(t('profile.extensions.detail.copied'))} /></div>
666
677
  <div class="ext-detail-code">${content.substring(0, 500)}${content.length > 500 ? '...' : ''}</div>
667
678
  </div>`;
668
679
  })}
@@ -674,10 +685,10 @@ export default function ExtensionsTab({ session, showToast }) {
674
685
  <div class="ext-detail-section">
675
686
  <div class="ext-detail-section-title">${'\u{1F4E6}'} Library: ${lib.filename}</div>
676
687
  <div class="ext-lib-meta">${t('profile.extensions.detail.exports')}: ${(lib.exports || []).join(', ')}</div>
677
- <div class="ext-lib-label">${t('profile.extensions.detail.scriptTag')} <button class="btn-primary btn-sm" onClick=${() => { copyToClipboard(scriptTag); showToast(t('profile.extensions.detail.copied')); }}>${t('profile.extensions.detail.copyUrl')}</button></div>
688
+ <div class="ext-lib-label">${t('profile.extensions.detail.scriptTag')} <${CopyButton} text=${scriptTag} label=${t('profile.extensions.detail.copyUrl')} className="btn-primary btn-sm" onCopied=${() => showToast(t('profile.extensions.detail.copied'))} /></div>
678
689
  <div class="ext-detail-code">${scriptTag}</div>
679
690
  ${lib.api_surface ? html`<div>
680
- <div class="ext-lib-label-spaced">${t('profile.extensions.detail.apiSurface')} <button class="btn-primary btn-sm" onClick=${() => { copyToClipboard(lib.api_surface); showToast(t('profile.extensions.detail.copied')); }}>${t('profile.extensions.detail.copyApi')}</button></div>
691
+ <div class="ext-lib-label-spaced">${t('profile.extensions.detail.apiSurface')} <${CopyButton} text=${lib.api_surface} label=${t('profile.extensions.detail.copyApi')} className="btn-primary btn-sm" onCopied=${() => showToast(t('profile.extensions.detail.copied'))} /></div>
681
692
  <div class="ext-detail-code">${lib.api_surface}</div>
682
693
  </div>` : null}
683
694
  </div>`;
@@ -726,7 +737,7 @@ export default function ExtensionsTab({ session, showToast }) {
726
737
  <div class="ext-description">${ext.description || ''}</div>
727
738
  <div class="ext-meta-row-sm">
728
739
  <span>${t('profile.v8ext.author')}: ${ext.author || '?'}</span>
729
- <span><span class="ext-status-dot ${ext.status}"></span> ${isActive ? t('profile.v8ext.statusActive') : t('profile.v8ext.statusInactive')}</span>
740
+ <span><${StatusDot} status=${ext.status} /> ${isActive ? t('profile.v8ext.statusActive') : t('profile.v8ext.statusInactive')}</span>
730
741
  <span>${t('profile.v8ext.actions')}: ${actions.length}</span>
731
742
  </div>
732
743
  </div>
@@ -829,9 +840,9 @@ export default function ExtensionsTab({ session, showToast }) {
829
840
  : srvInstances.map(inst => html`
830
841
  <div class="ext-instance-row">
831
842
  <span class="ext-instance-name">${inst.id}</span>
832
- <span class="ext-status-dot ${inst.status}"></span>
843
+ <${StatusDot} status=${inst.status} />
833
844
  <span class="ext-instance-status">${inst.status}</span>
834
- <button class="btn-outline btn-sm" onClick=${() => { copyToClipboard(inst.id); showToast(t('profile.v8ext.instanceIdCopied')); }}>${t('profile.v8ext.copyId')}</button>
845
+ <${CopyButton} text=${inst.id} label=${t('profile.v8ext.copyId')} className="btn-outline btn-sm" onCopied=${() => showToast(t('profile.v8ext.instanceIdCopied'))} />
835
846
  <button class="btn-danger-solid btn-sm" onClick=${() => handleDeleteInstance(ext.name, inst.id)}>${t('profile.v8ext.deleteInstance')}</button>
836
847
  </div>`)}
837
848
  ${isActive ? html`
@@ -845,7 +856,7 @@ export default function ExtensionsTab({ session, showToast }) {
845
856
  <div class="ext-endpoint-box">
846
857
  POST ${NODE_URL}/v1/ext/${ext.name}/{actionId}
847
858
  </div>
848
- <button class="btn-primary btn-sm ext-btn-copy-spaced" onClick=${() => { copyToClipboard(NODE_URL + '/v1/ext/' + ext.name + '/'); showToast(t('profile.v8ext.copied')); }}>${t('profile.v8ext.copyEndpoint')}</button>
859
+ <${CopyButton} text=${NODE_URL + '/v1/ext/' + ext.name + '/'} label=${t('profile.v8ext.copyEndpoint')} className="btn-primary btn-sm ext-btn-copy-spaced" onCopied=${() => showToast(t('profile.v8ext.copied'))} />
849
860
  </div>`}
850
861
 
851
862
  <div class="ext-actions-bar">
@@ -893,7 +904,7 @@ export default function ExtensionsTab({ session, showToast }) {
893
904
  ${(ext.actions || []).map(a => html`<span class="ext-comp-tag ext-comp-tag-action">${a.id}</span>`)}
894
905
  </div>
895
906
  <div class="ext-card-footer">
896
- <span class="ext-status"><span class="ext-status-dot ${ext.status}"></span> ${isActive ? t('profile.v8ext.statusActive') : t('profile.v8ext.statusInactive')}</span>
907
+ <span class="ext-status"><${StatusDot} status=${ext.status} /> ${isActive ? t('profile.v8ext.statusActive') : t('profile.v8ext.statusInactive')}</span>
897
908
  <span class="ext-card-actions">
898
909
  ${isActive
899
910
  ? html`<button onClick=${(e) => { e.stopPropagation(); handleSrvDeactivate(ext.name); }}>${t('profile.v8ext.deactivate')}</button>`
@@ -935,7 +946,7 @@ export default function ExtensionsTab({ session, showToast }) {
935
946
  ${types.map(ct => html`<span class="ext-comp-tag ${COMP_TAG_CLASSES[ct] || ''}">${t('profile.extensions.components.' + ct) || ct}</span>`)}
936
947
  </div>
937
948
  <div class="ext-card-footer">
938
- <span class="ext-status"><span class="ext-status-dot ${ext.status}"></span> ${t('profile.extensions.status.' + ext.status)}</span>
949
+ <span class="ext-status"><${StatusDot} status=${ext.status} /> ${t('profile.extensions.status.' + ext.status)}</span>
939
950
  </div>
940
951
  </div>`;
941
952
  })}
@@ -961,7 +972,7 @@ export default function ExtensionsTab({ session, showToast }) {
961
972
  ${types.map(ct => html`<span class="ext-comp-tag ${COMP_TAG_CLASSES[ct] || ''}">${t('profile.extensions.components.' + ct) || ct}</span>`)}
962
973
  </div>
963
974
  <div class="ext-card-footer">
964
- <span class="ext-status"><span class="ext-status-dot ${ext.status}"></span> ${t('profile.extensions.status.' + ext.status)}</span>
975
+ <span class="ext-status"><${StatusDot} status=${ext.status} /> ${t('profile.extensions.status.' + ext.status)}</span>
965
976
  <span class="ext-card-actions">
966
977
  ${isActive
967
978
  ? html`<button onClick=${(e) => { e.stopPropagation(); deactivateExt(ext.name); }}>${t('profile.extensions.deactivate')}</button>`
@@ -995,9 +1006,7 @@ export default function ExtensionsTab({ session, showToast }) {
995
1006
  </div>` : null}
996
1007
 
997
1008
  ${showInstall ? html`
998
- <div class="modal-overlay" onClick=${(e) => { if (e.target === e.currentTarget) setShowInstall(false); }}>
999
- <div class="modal ext-modal-narrow">
1000
- <h3>${t('profile.extensions.installModal.title')}</h3>
1009
+ <${Modal} open=${true} onClose=${() => setShowInstall(false)} title=${t('profile.extensions.installModal.title')} className="ext-modal-narrow">
1001
1010
  <form onSubmit=${handleInstall}>
1002
1011
  <div class="ext-modal-section">
1003
1012
  <label>${t('profile.extensions.installModal.manifestLabel')}</label>
@@ -1033,13 +1042,10 @@ export default function ExtensionsTab({ session, showToast }) {
1033
1042
  <button type="submit" class="btn-primary">${t('profile.extensions.installModal.installBtn')}</button>
1034
1043
  </div>
1035
1044
  </form>
1036
- </div>
1037
- </div>` : null}
1045
+ <//>` : null}
1038
1046
 
1039
1047
  ${showSrvInstall ? html`
1040
- <div class="modal-overlay" onClick=${(e) => { if (e.target === e.currentTarget) setShowSrvInstall(false); }}>
1041
- <div class="modal ext-modal-narrow">
1042
- <h3>Install Server Extension</h3>
1048
+ <${Modal} open=${true} onClose=${() => setShowSrvInstall(false)} title="Install Server Extension" className="ext-modal-narrow">
1043
1049
  <div class="ext-modal-section">
1044
1050
  <label>YAML Manifest</label>
1045
1051
  <textarea rows="12" class="ext-modal-textarea" placeholder="metadata:\n name: my-extension\n version: 1.0.0\n description: ...\n\nactions:\n - id: my-action\n ..." value=${srvManifestText} onInput=${(e) => onSrvManifestChange(e.target.value)}></textarea>
@@ -1057,8 +1063,7 @@ export default function ExtensionsTab({ session, showToast }) {
1057
1063
  <button class="btn-outline" onClick=${() => setShowSrvInstall(false)}>Cancel</button>
1058
1064
  <button class="btn-primary" onClick=${handleSrvInstall}>Install</button>
1059
1065
  </div>
1060
- </div>
1061
- </div>` : null}
1066
+ <//>` : null}
1062
1067
 
1063
1068
  <${ConfirmUI} />
1064
1069
  </div>`;
@@ -4,6 +4,8 @@
4
4
  * @version-history
5
5
  * v1.0.0 — 2026-03-16 — Initial federation tab
6
6
  * v1.1.0 — 2026-03-17 — Replace inline styles with CSS classes
7
+ * v1.2.0 — 2026-06-02 — Component unification (#11): peer online/offline dot
8
+ * uses canonical <StatusDot> (alive/dead) instead of bespoke .peer-dot.
7
9
  */
8
10
  import { h } from 'preact';
9
11
  import { useState, useEffect, useRef } from 'preact/hooks';
@@ -12,6 +14,7 @@ const html = htm.bind(h);
12
14
  import { t } from '/js/i18n.js';
13
15
  import { escHtml } from '/js/utils.js';
14
16
  import { Spinner } from './shared.js';
17
+ import { StatusDot } from '/components/StatusDot.js';
15
18
  import { listPeers } from '/js/services/federation.js';
16
19
 
17
20
  export default function FederationTab({ session, showToast }) {
@@ -53,7 +56,7 @@ export default function FederationTab({ session, showToast }) {
53
56
  <div class="card-subtitle">${escHtml(p.url || '')}</div>
54
57
  </div>
55
58
  <div class="peer-status">
56
- <span class="peer-dot ${alive ? 'alive' : 'dead'}"></span>
59
+ <${StatusDot} status=${alive ? 'alive' : 'dead'} />
57
60
  <span class="fed-status-text ${alive ? 'online' : 'offline'}">${alive ? t('profile.federation.online') : t('profile.federation.offline')}</span>
58
61
  </div>
59
62
  </div>
@@ -28,6 +28,8 @@ import htm from 'htm';
28
28
  const html = htm.bind(h);
29
29
  import { t } from '/js/i18n.js';
30
30
  import { apiPost } from '/js/api.js';
31
+ import { copyToClipboard } from '/js/utils.js';
32
+ import { CopyButton } from '/components/CopyButton.js';
31
33
  import {
32
34
  saveComponent, registerComponent, reregisterComponent, writeProjectLog, writeDebugArtifact,
33
35
  getNextPass, MULTI_PASS_TYPES, saveSkeleton, saveUnit, saveTestCode,
@@ -637,7 +639,7 @@ export function ComponentDetail({ component, project, components, projectId, int
637
639
  interviewSpec,
638
640
  );
639
641
  try {
640
- await navigator.clipboard.writeText(fresh);
642
+ await copyToClipboard(fresh);
641
643
  const updated = addHistory(component, 'prompt_copied');
642
644
  await saveComponent(projectId, { ...updated, status: 'waiting_user', prompt: fresh });
643
645
  showToast?.(t('profile.foundry.promptCopied'));
@@ -719,7 +721,7 @@ export function ComponentDetail({ component, project, components, projectId, int
719
721
  async function handleCopyTestPrompt() {
720
722
  if (!currentTestPrompt) return;
721
723
  try {
722
- await navigator.clipboard.writeText(currentTestPrompt);
724
+ await copyToClipboard(currentTestPrompt);
723
725
  showToast?.(t('profile.foundry.test_prompt_copied'));
724
726
  await writeProjectLog(projectId, 'test_prompt_copied', { meta: { component: component.label, by: 'user' } });
725
727
  } catch { /* clipboard fallback */ }
@@ -817,14 +819,6 @@ export function ComponentDetail({ component, project, components, projectId, int
817
819
  // Multi-pass: current pass and submit handler
818
820
  const currentPass = isMultiPass && component.passes ? getNextPass(component.passes) : null;
819
821
 
820
- async function handlePassCopy() {
821
- if (!passPrompt) return;
822
- try {
823
- await navigator.clipboard.writeText(passPrompt);
824
- showToast?.(t('profile.foundry.promptCopied'));
825
- } catch { /* clipboard fallback */ }
826
- }
827
-
828
822
  async function handlePassRunWithAi() {
829
823
  if (!currentPass || !passPrompt) return;
830
824
  setAiRunning(true);
@@ -899,9 +893,11 @@ export function ComponentDetail({ component, project, components, projectId, int
899
893
  <label>${t('profile.foundry.passProgress')} — ${t('profile.foundry.' + (({'test':'passTest','skeleton':'passSkeleton','unit':'passUnit','assembly':'passAssembly','reflection':'passReflection'})[currentPass.type] || currentPass.type))}${currentPass.unitLabel ? ': ' + currentPass.unitLabel : ''}</label>
900
894
  <pre class="fnd-prompt-box">${passPrompt}</pre>
901
895
  <div class="flex-row-wrap">
902
- <button class="btn-outline btn-sm" onClick=${handlePassCopy}>
903
- ${t('profile.foundry.copyPrompt')}
904
- </button>
896
+ <${CopyButton}
897
+ text=${passPrompt}
898
+ label=${t('profile.foundry.copyPrompt')}
899
+ className="btn-outline btn-sm"
900
+ onCopied=${() => showToast?.(t('profile.foundry.promptCopied'))} />
905
901
  ${orSettings?.hasApiKey && html`
906
902
  <button class="btn-outline btn-sm fnd-or-run-btn ${aiRunning ? 'fnd-or-running' : ''}"
907
903
  onClick=${handlePassRunWithAi}
@@ -982,10 +978,11 @@ export function ComponentDetail({ component, project, components, projectId, int
982
978
  />
983
979
  <div class="fnd-actions">
984
980
  ${explainPrompt && html`
985
- <button class="btn-ghost btn-sm" onClick=${() => navigator.clipboard.writeText(explainPrompt)}
986
- title="Copy explain prompt — ask AI to describe what it built before validating">
987
- Explain
988
- </button>
981
+ <${CopyButton}
982
+ text=${explainPrompt}
983
+ label="Explain"
984
+ className="btn-ghost btn-sm"
985
+ title="Copy explain prompt — ask AI to describe what it built before validating" />
989
986
  `}
990
987
  ${workflowStep === 'validate' && html`<${StepArrow} />`}
991
988
  <button class="btn-primary btn-sm" onClick=${handleValidate} disabled=${!result.trim()}
@@ -1017,17 +1014,19 @@ export function ComponentDetail({ component, project, components, projectId, int
1017
1014
  </ul>
1018
1015
  ${reflectionPrompt && html`
1019
1016
  <div class="flex-row" style="margin-bottom: 0.5rem">
1020
- <button class="btn-outline btn-sm" onClick=${() => navigator.clipboard.writeText(reflectionPrompt)}>
1021
- 1. Copy Reflection (diagnose first)
1022
- </button>
1017
+ <${CopyButton}
1018
+ text=${reflectionPrompt}
1019
+ label="1. Copy Reflection (diagnose first)"
1020
+ className="btn-outline btn-sm" />
1023
1021
  </div>
1024
1022
  `}
1025
1023
  ${fixPrompt && html`
1026
1024
  <div class="flex-row">
1027
1025
  ${workflowStep === 'fix' && html`<${StepArrow} />`}
1028
- <button class="btn-primary btn-sm" onClick=${() => navigator.clipboard.writeText(fixPrompt)}>
1029
- 2. ${t('profile.foundry.copyFixPrompt')}
1030
- </button>
1026
+ <${CopyButton}
1027
+ text=${fixPrompt}
1028
+ label=${'2. ' + t('profile.foundry.copyFixPrompt')}
1029
+ className="btn-primary btn-sm" />
1031
1030
  </div>
1032
1031
  `}
1033
1032
  </div>
@@ -52,6 +52,8 @@ import htm from 'htm';
52
52
  const html = htm.bind(h);
53
53
  import { t, getLocale } from '/js/i18n.js';
54
54
  import { apiGet } from '/js/api.js';
55
+ import { copyToClipboard } from '/js/utils.js';
56
+ import { CopyButton } from '/components/CopyButton.js';
55
57
  import {
56
58
  listProjects, createProject, updateProject, archiveProject,
57
59
  loadAllComponents, saveComponent,
@@ -168,7 +170,7 @@ function NewProjectView({ onBack, onCreated, showToast, orSettings }) {
168
170
  }
169
171
  } catch { /* proceed without catalog */ }
170
172
  const prompt = buildBlueprintPrompt(description, interviewParsed, cortexLibs);
171
- navigator.clipboard.writeText(prompt).catch(() => {});
173
+ await copyToClipboard(prompt);
172
174
  showToast?.(t('profile.foundry.blueprintPromptCopied'));
173
175
  }
174
176
 
@@ -200,12 +202,6 @@ function NewProjectView({ onBack, onCreated, showToast, orSettings }) {
200
202
  }
201
203
 
202
204
  if (phase === 'interview') {
203
- function handleCopyInterviewPrompt() {
204
- const prompt = buildInterviewPrompt(description, getLocale());
205
- navigator.clipboard.writeText(prompt).catch(() => {});
206
- showToast?.(t('profile.foundry.interviewPromptCopied'));
207
- }
208
-
209
205
  async function handleRunInterviewAi() {
210
206
  if (!project) return;
211
207
  setAiRunning('interview');
@@ -289,9 +285,11 @@ function NewProjectView({ onBack, onCreated, showToast, orSettings }) {
289
285
  <div class="fnd-section">
290
286
  <label>${t('profile.foundry.interviewPrompt')}</label>
291
287
  <div class="fnd-or-btn-row">
292
- <button class="btn-primary" onClick=${handleCopyInterviewPrompt}>
293
- ${t('profile.foundry.copyPrompt')}
294
- </button>
288
+ <${CopyButton}
289
+ text=${buildInterviewPrompt(description, getLocale())}
290
+ label=${t('profile.foundry.copyPrompt')}
291
+ className="btn-primary"
292
+ onCopied=${() => showToast?.(t('profile.foundry.interviewPromptCopied'))} />
295
293
  ${orSettings?.hasApiKey && html`
296
294
  <button class="btn-outline fnd-or-run-btn ${aiRunning === 'interview' ? 'fnd-or-running' : ''}"
297
295
  onClick=${handleRunInterviewAi}
@@ -430,9 +428,10 @@ function NewProjectView({ onBack, onCreated, showToast, orSettings }) {
430
428
  <label>${t('profile.foundry.errors')}</label>
431
429
  <ul>${blueprintErrors.map(e => html`<li>${e}</li>`)}</ul>
432
430
  ${fixPrompt && html`
433
- <button class="btn-primary btn-sm" onClick=${() => navigator.clipboard.writeText(fixPrompt)}>
434
- ${t('profile.foundry.copyFixPrompt')}
435
- </button>
431
+ <${CopyButton}
432
+ text=${fixPrompt}
433
+ label=${t('profile.foundry.copyFixPrompt')}
434
+ className="btn-primary btn-sm" />
436
435
  `}
437
436
  </div>
438
437
  `}
@@ -22,6 +22,8 @@ import htm from 'htm';
22
22
  const html = htm.bind(h);
23
23
  import { t } from '/js/i18n.js';
24
24
  import { apiGet, apiDelete } from '/js/api.js';
25
+ import { copyToClipboard } from '/js/utils.js';
26
+ import { CopyButton } from '/components/CopyButton.js';
25
27
 
26
28
  export function DebugPanel({ projectId }) {
27
29
  const [files, setFiles] = useState([]);
@@ -105,7 +107,7 @@ export function DebugPanel({ projectId }) {
105
107
  }
106
108
  }
107
109
 
108
- await navigator.clipboard.writeText(parts.join('\n'));
110
+ await copyToClipboard(parts.join('\n'));
109
111
  // Brief visual feedback
110
112
  setCopying('done');
111
113
  setTimeout(() => setCopying(false), 2000);
@@ -175,12 +177,7 @@ export function DebugPanel({ projectId }) {
175
177
  ${selectedFile ? html`
176
178
  <div class="pf-gen-debug-content-header">
177
179
  <span class="pf-gen-debug-path">${selectedFile}</span>
178
- <button class="btn-outline btn-sm pf-gen-debug-copy-file" onClick=${() => {
179
- if (fileContent) {
180
- navigator.clipboard.writeText(fileContent);
181
- // TODO: visual feedback
182
- }
183
- }}>Copy File</button>
180
+ <${CopyButton} text=${fileContent || ''} label="Copy File" copiedLabel="Copied!" className="btn-outline btn-sm pf-gen-debug-copy-file" />
184
181
  </div>
185
182
  ${loadingFile
186
183
  ? html`<p>${t('profile.loading')}</p>`
@@ -78,7 +78,10 @@ export function useDashboardCore(projectId, onBack, showToast) {
78
78
  // Blueprint exists but no component records yet — initialize them
79
79
  const initialized = [];
80
80
  for (const c of p.blueprint.components) {
81
- const comp = { id: c.id, type: c.type, label: c.label, status: 'not_started', prompt: null, result: null, validationErrors: [], registeredAs: null, history: [], _version: 0 };
81
+ // Preserve subtype (data|component|app-domain) on cortex records — the spec/code prompt
82
+ // selectors and the spec validators dispatch on it. Dropping it here was the single hop
83
+ // where subtype was lost end-to-end, forcing every downstream picker onto a wrong guess.
84
+ const comp = { id: c.id, type: c.type, label: c.label, ...(c.subtype ? { subtype: c.subtype } : {}), status: 'not_started', prompt: null, result: null, validationErrors: [], registeredAs: null, history: [], _version: 0 };
82
85
  await saveComponent(projectId, comp);
83
86
  initialized.push(comp);
84
87
  }
@@ -30,6 +30,7 @@
30
30
  */
31
31
  import { useState, useEffect } from 'preact/hooks';
32
32
  import { t } from '/js/i18n.js';
33
+ import { copyToClipboard } from '/js/utils.js';
33
34
  import {
34
35
  loadAllComponents, saveComponent, registerComponent, writeProjectLog,
35
36
  savePendingEdit, clearPendingEdit,
@@ -80,7 +81,7 @@ export function useEditMode(core, autopilotState, projectId, orSettings, session
80
81
 
81
82
  function handleCopyImpactPrompt() {
82
83
  const prompt = buildImpactPrompt(changeRequest, core.project?.blueprint);
83
- navigator.clipboard.writeText(prompt).catch(() => {});
84
+ copyToClipboard(prompt).catch(() => {});
84
85
  showToast?.(t('profile.generator.impactPromptCopied'));
85
86
  setEditMode('impact');
86
87
  }
@@ -225,7 +226,7 @@ export function useEditMode(core, autopilotState, projectId, orSettings, session
225
226
  suggestedChange || changeRequest,
226
227
  upstream || null,
227
228
  );
228
- navigator.clipboard.writeText(prompt).catch(() => {});
229
+ copyToClipboard(prompt).catch(() => {});
229
230
  showToast?.(t('profile.generator.editPromptCopied').replace('{name}', comp.label));
230
231
  }
231
232
 
@@ -26,6 +26,8 @@ import htm from 'htm';
26
26
  const html = htm.bind(h);
27
27
  import { t } from '/js/i18n.js';
28
28
  import { apiPost } from '/js/api.js';
29
+ import { copyToClipboard } from '/js/utils.js';
30
+ import { CopyButton } from '/components/CopyButton.js';
29
31
  import {
30
32
  saveComponent, saveSpec, registerComponent, reregisterComponent, writeProjectLog, writeDebugArtifact,
31
33
  } from '/js/services/generator.js';
@@ -431,7 +433,7 @@ export function ComponentDetail({ component, project, components, projectId, int
431
433
  async function handleCopyPrompt() {
432
434
  const fresh = await loadPromptFromBackend(projectId, component.id, 'code');
433
435
  try {
434
- await navigator.clipboard.writeText(fresh);
436
+ await copyToClipboard(fresh);
435
437
  const updated = addHistory(component, 'prompt_copied');
436
438
  await saveComponent(projectId, { ...updated, status: 'waiting_user', prompt: fresh });
437
439
  showToast?.(t('profile.generator.promptCopied'));
@@ -498,6 +500,14 @@ export function ComponentDetail({ component, project, components, projectId, int
498
500
  ? buildExplainPrompt(component.type, result, bpComp)
499
501
  : null;
500
502
 
503
+ // Spec fix prompt — derived from current spec validation errors + spec result (shared by Copy and Fix-with-AI buttons)
504
+ const specFixPrompt = specValidation && !specValidation.valid
505
+ ? 'Fix the following spec JSON. It has validation errors.\n\n'
506
+ + '## Errors\n' + specValidation.errors.map((e, i) => (i + 1) + '. ' + e).join('\n')
507
+ + '\n\n## Current Spec\n```json\n' + specResult + '\n```\n\n'
508
+ + '## Rules\n- Fix ONLY the listed errors\n- Do NOT remove existing actions\n- Return the COMPLETE fixed JSON'
509
+ : null;
510
+
501
511
  // Test prompt for testable component types
502
512
  const testableTypes = ['extension', 'cortex', 'app'];
503
513
  const isTestable = testableTypes.includes(component.type) && component.registeredAs;
@@ -518,7 +528,7 @@ export function ComponentDetail({ component, project, components, projectId, int
518
528
  async function handleCopyTestPrompt() {
519
529
  if (!currentTestPrompt) return;
520
530
  try {
521
- await navigator.clipboard.writeText(currentTestPrompt);
531
+ await copyToClipboard(currentTestPrompt);
522
532
  showToast?.(t('profile.generator.test_prompt_copied'));
523
533
  await writeProjectLog(projectId, 'test_prompt_copied', { meta: { component: component.label, by: 'user' } });
524
534
  } catch { /* clipboard fallback */ }
@@ -633,9 +643,8 @@ export function ComponentDetail({ component, project, components, projectId, int
633
643
  <label>SPEC PROMPT</label>
634
644
  <pre class="pf-gen-prompt-box" style="max-height:200px;overflow:auto;font-size:11px">${specPrompt || 'Loading spec prompt...'}</pre>
635
645
  <div class="flex-row-wrap">
636
- <button class="btn-outline btn-sm" onClick=${() => navigator.clipboard.writeText(specPrompt).then(() => showToast?.('Spec prompt copied'))}>
637
- Copy Spec Prompt
638
- </button>
646
+ <${CopyButton} text=${specPrompt} label="Copy Spec Prompt" className="btn-outline btn-sm"
647
+ onCopied=${() => showToast?.('Spec prompt copied')} />
639
648
  ${orSettings?.hasApiKey && html`
640
649
  <button class="btn-outline btn-sm pf-gen-or-run-btn ${specAiRunning ? 'pf-gen-or-running' : ''}"
641
650
  onClick=${async () => {
@@ -676,7 +685,11 @@ export function ComponentDetail({ component, project, components, projectId, int
676
685
  else if (component.subtype === 'data') sv = validateDataApiSpec(parsed);
677
686
  else if (component.subtype === 'component') sv = validateComponentSpec(parsed);
678
687
  else if (component.subtype === 'app-domain') sv = validateAppDomainSpec(parsed);
679
- else sv = validateDataApiSpec(parsed); // fallback
688
+ // FAIL LOUD: a cortex must declare its subtype. A silent fallback to a default
689
+ // validator (previously validateDataApiSpec) masked the real bug — a missing subtype —
690
+ // as a confusing "Missing wrapsExtension" error on a component cortex. Name the real cause.
691
+ else if (component.type === 'cortex') sv = { valid: false, errors: ['Cortex component "' + component.id + '" has no subtype (expected data | component | app-domain) — this is an upstream blueprint/init bug; fix the subtype, do not guess a validator'] };
692
+ else sv = { valid: false, errors: ['Cannot select a spec validator for component "' + component.id + '" (type=' + component.type + ', subtype=' + component.subtype + ')'] };
680
693
  // Also check blueprint action IDs
681
694
  const bp = project?.blueprint;
682
695
  if (bp && component.type === 'extension') {
@@ -730,23 +743,12 @@ export function ComponentDetail({ component, project, components, projectId, int
730
743
  <strong>Spec Validation Errors</strong>
731
744
  <ul>${specValidation.errors.map(e => html`<li>${e}</li>`)}</ul>
732
745
  <div class="flex-row-wrap" style="margin-top:8px">
733
- <button class="btn-outline btn-sm" onClick=${() => {
734
- const fixPrompt = 'Fix the following spec JSON. It has validation errors.\n\n'
735
- + '## Errors\n' + specValidation.errors.map((e, i) => (i + 1) + '. ' + e).join('\n')
736
- + '\n\n## Current Spec\n```json\n' + specResult + '\n```\n\n'
737
- + '## Rules\n- Fix ONLY the listed errors\n- Do NOT remove existing actions\n- Return the COMPLETE fixed JSON';
738
- navigator.clipboard.writeText(fixPrompt);
739
- showToast?.('Fix prompt copied');
740
- }}>
741
- Copy Fix Prompt
742
- </button>
746
+ <${CopyButton} text=${specFixPrompt} label="Copy Fix Prompt" className="btn-outline btn-sm"
747
+ onCopied=${() => showToast?.('Fix prompt copied')} />
743
748
  ${orSettings?.hasApiKey && html`
744
749
  <button class="btn-outline btn-sm pf-gen-or-run-btn ${specAiRunning ? 'pf-gen-or-running' : ''}"
745
750
  onClick=${async () => {
746
- const fixPrompt = 'Fix the following spec JSON. It has validation errors.\n\n'
747
- + '## Errors\n' + specValidation.errors.map((e, i) => (i + 1) + '. ' + e).join('\n')
748
- + '\n\n## Current Spec\n```json\n' + specResult + '\n```\n\n'
749
- + '## Rules\n- Fix ONLY the listed errors\n- Do NOT remove existing actions\n- Return the COMPLETE fixed JSON';
751
+ const fixPrompt = specFixPrompt;
750
752
  setSpecAiRunning(true);
751
753
  try {
752
754
  const fixed = await runWithAi(projectId, fixPrompt);
@@ -819,10 +821,8 @@ export function ComponentDetail({ component, project, components, projectId, int
819
821
  />
820
822
  <div class="pf-gen-actions">
821
823
  ${explainPrompt && html`
822
- <button class="btn-ghost btn-sm" onClick=${() => navigator.clipboard.writeText(explainPrompt)}
823
- title="Copy explain prompt — ask AI to describe what it built before validating">
824
- Explain
825
- </button>
824
+ <${CopyButton} text=${explainPrompt} label="Explain" className="btn-ghost btn-sm"
825
+ title="Copy explain prompt — ask AI to describe what it built before validating" />
826
826
  `}
827
827
  ${workflowStep === 'validate' && html`<${StepArrow} />`}
828
828
  <button class="btn-primary btn-sm" onClick=${handleValidate} disabled=${!result.trim()}
@@ -854,17 +854,13 @@ export function ComponentDetail({ component, project, components, projectId, int
854
854
  </ul>
855
855
  ${reflectionPrompt && html`
856
856
  <div class="flex-row" style="margin-bottom: 0.5rem">
857
- <button class="btn-outline btn-sm" onClick=${() => navigator.clipboard.writeText(reflectionPrompt)}>
858
- 1. Copy Reflection (diagnose first)
859
- </button>
857
+ <${CopyButton} text=${reflectionPrompt} label="1. Copy Reflection (diagnose first)" className="btn-outline btn-sm" />
860
858
  </div>
861
859
  `}
862
860
  ${fixPrompt && html`
863
861
  <div class="flex-row">
864
862
  ${workflowStep === 'fix' && html`<${StepArrow} />`}
865
- <button class="btn-primary btn-sm" onClick=${() => navigator.clipboard.writeText(fixPrompt)}>
866
- 2. ${t('profile.generator.copyFixPrompt')}
867
- </button>
863
+ <${CopyButton} text=${fixPrompt} label=${'2. ' + t('profile.generator.copyFixPrompt')} className="btn-primary btn-sm" />
868
864
  </div>
869
865
  `}
870
866
  </div>
@@ -52,6 +52,8 @@ import htm from 'htm';
52
52
  const html = htm.bind(h);
53
53
  import { t, getLocale } from '/js/i18n.js';
54
54
  import { apiGet } from '/js/api.js';
55
+ import { copyToClipboard } from '/js/utils.js';
56
+ import { CopyButton } from '/components/CopyButton.js';
55
57
  import {
56
58
  listProjects, createProject, updateProject, archiveProject,
57
59
  loadAllComponents, saveComponent,
@@ -168,7 +170,7 @@ function NewProjectView({ onBack, onCreated, showToast, orSettings }) {
168
170
  async function handleCopyBlueprintPrompt() {
169
171
  try {
170
172
  const prompt = await loadGeneratorPrompt(project.projectId, 'blueprint');
171
- navigator.clipboard.writeText(prompt).catch(() => {});
173
+ copyToClipboard(prompt).catch(() => {});
172
174
  showToast?.(t('profile.generator.blueprintPromptCopied'));
173
175
  } catch (e) {
174
176
  showToast?.(e.message, true);
@@ -206,7 +208,7 @@ function NewProjectView({ onBack, onCreated, showToast, orSettings }) {
206
208
  async function handleCopyInterviewPrompt() {
207
209
  try {
208
210
  const prompt = await loadGeneratorPrompt(project.projectId, 'interview', getLocale());
209
- navigator.clipboard.writeText(prompt).catch(() => {});
211
+ copyToClipboard(prompt).catch(() => {});
210
212
  showToast?.(t('profile.generator.interviewPromptCopied'));
211
213
  } catch (e) {
212
214
  showToast?.(e.message, true);
@@ -428,9 +430,7 @@ function NewProjectView({ onBack, onCreated, showToast, orSettings }) {
428
430
  <label>${t('profile.generator.errors')}</label>
429
431
  <ul>${blueprintErrors.map(e => html`<li>${e}</li>`)}</ul>
430
432
  ${fixPrompt && html`
431
- <button class="btn-primary btn-sm" onClick=${() => navigator.clipboard.writeText(fixPrompt)}>
432
- ${t('profile.generator.copyFixPrompt')}
433
- </button>
433
+ <${CopyButton} text=${fixPrompt} label=${t('profile.generator.copyFixPrompt')} className="btn-primary btn-sm" />
434
434
  `}
435
435
  </div>
436
436
  `}