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
@@ -6,16 +6,67 @@
6
6
  * @version-history
7
7
  * v1.0.0 -- 2026-05-24 -- Initial creation for Agent Detail Tab-View
8
8
  * v1.1.0 -- 2026-05-24 -- Add edit/copy/download buttons (F8), edit mode (F9), upload (F10), file metadata (F11)
9
+ * v1.2.0 -- 2026-06-02 -- Component unification: route handleCopy through the shared
10
+ * copyToClipboard (/js/utils.js) instead of raw navigator.clipboard.writeText, so the
11
+ * insecure-context fallback applies; toast preserved (TIER 3, stays a handler)
9
12
  */
10
13
 
11
14
  import { h } from 'preact';
12
15
  import { useState, useEffect, useRef } from 'preact/hooks';
13
16
  import htm from 'htm';
14
17
  import { t } from '/js/i18n.js';
15
- import { apiGet, apiPut } from '/js/api.js';
18
+ import { copyToClipboard } from '/js/utils.js';
19
+ import { apiGet, apiPut, apiPatch } from '/js/api.js';
16
20
 
17
21
  const html = htm.bind(h);
18
22
 
23
+ /** Opt-in budget guards applied as defaults to schedules created for this agent. */
24
+ function ScheduleBudgetSection({ agent, agentName, showToast }) {
25
+ const defaults = Array.isArray(agent?.schedule_constraint_defaults) ? agent.schedule_constraint_defaults : [];
26
+ const findC = (type) => defaults.find(c => c.type === type);
27
+ const [maxRuns, setMaxRuns] = useState({ enabled: !!findC('max_runs')?.enabled, limit: findC('max_runs')?.params?.limit ?? 7 });
28
+ const [dailyLimit, setDailyLimit] = useState({ enabled: !!findC('daily_limit')?.enabled, limit: agent?.daily_spend_limit ?? findC('daily_limit')?.params?.limit ?? 1 });
29
+ const [saving, setSaving] = useState(false);
30
+
31
+ const save = async () => {
32
+ const constraints = [];
33
+ if (maxRuns.enabled) constraints.push({ type: 'max_runs', enabled: true, params: { limit: Number(maxRuns.limit) } });
34
+ if (dailyLimit.enabled) constraints.push({ type: 'daily_limit', enabled: true, params: { limit: Number(dailyLimit.limit) } });
35
+ setSaving(true);
36
+ try {
37
+ await apiPatch(`/v1/agents/${encodeURIComponent(agentName)}/schedule-constraints`, {
38
+ constraints,
39
+ daily_spend_limit: dailyLimit.enabled ? Number(dailyLimit.limit) : null,
40
+ });
41
+ showToast?.(t('profile.scheduler.budgetSaved'));
42
+ } catch (e) { showToast?.(e.message, true); }
43
+ finally { setSaving(false); }
44
+ };
45
+
46
+ return html`
47
+ <div class="sch-form sch-budget-section">
48
+ <div class="section-title">${t('profile.scheduler.budgetTitle')}</div>
49
+ <div class="section-desc">${t('profile.scheduler.budgetDesc')}</div>
50
+ <div class="sch-constraints">
51
+ <label class="sch-check">
52
+ <input type="checkbox" checked=${maxRuns.enabled} onChange=${e => setMaxRuns(s => ({ ...s, enabled: e.target.checked }))} />
53
+ ${t('profile.scheduler.maxRuns')}
54
+ <input type="number" min="1" value=${maxRuns.limit} disabled=${!maxRuns.enabled}
55
+ onInput=${e => setMaxRuns(s => ({ ...s, limit: e.target.value }))} class="sch-num" />
56
+ </label>
57
+ <label class="sch-check">
58
+ <input type="checkbox" checked=${dailyLimit.enabled} onChange=${e => setDailyLimit(s => ({ ...s, enabled: e.target.checked }))} />
59
+ ${t('profile.scheduler.dailyLimit')}
60
+ <input type="number" min="0" step="0.1" value=${dailyLimit.limit} disabled=${!dailyLimit.enabled}
61
+ onInput=${e => setDailyLimit(s => ({ ...s, limit: e.target.value }))} class="sch-num" />
62
+ </label>
63
+ </div>
64
+ <div class="sch-form-actions">
65
+ <button class="btn-primary btn-sm" disabled=${saving} onClick=${save}>${saving ? t('profile.scheduler.saving') : t('profile.scheduler.budgetSave')}</button>
66
+ </div>
67
+ </div>`;
68
+ }
69
+
19
70
  export default function TabAgentConfig({ agent, agentName, session, showToast }) {
20
71
  const [files, setFiles] = useState([]);
21
72
  const [selectedFile, setSelectedFile] = useState(null);
@@ -25,8 +76,8 @@ export default function TabAgentConfig({ agent, agentName, session, showToast })
25
76
  const [editContent, setEditContent] = useState('');
26
77
  const fileInputRef = useRef(null);
27
78
 
28
- async function loadFiles() {
29
- setLoading(true);
79
+ async function loadFiles({ showSpinner = true } = {}) {
80
+ if (showSpinner) setLoading(true);
30
81
  try {
31
82
  const gaii = agent?.gaii || agentName;
32
83
  const resp = await apiGet(`/v1/memory?prefix=agents.config.&agent=${encodeURIComponent(gaii)}`);
@@ -46,7 +97,7 @@ export default function TabAgentConfig({ agent, agentName, session, showToast })
46
97
  setPreview(configFiles[0].content);
47
98
  }
48
99
  } catch {
49
- setFiles([]);
100
+ if (showSpinner) setFiles([]); // keep old files on a transient live-update refetch
50
101
  }
51
102
  setLoading(false);
52
103
  }
@@ -56,7 +107,8 @@ export default function TabAgentConfig({ agent, agentName, session, showToast })
56
107
  const loadRef = useRef(loadFiles);
57
108
  loadRef.current = loadFiles;
58
109
  useEffect(() => {
59
- const handler = () => loadRef.current();
110
+ // Silent on live-update so the tab doesn't flash blank; first mount shows the spinner.
111
+ const handler = () => loadRef.current({ showSpinner: false });
60
112
  window.addEventListener('aimeat-live-update', handler);
61
113
  return () => window.removeEventListener('aimeat-live-update', handler);
62
114
  }, []);
@@ -90,7 +142,7 @@ export default function TabAgentConfig({ agent, agentName, session, showToast })
90
142
  }
91
143
 
92
144
  function handleCopy() {
93
- navigator.clipboard.writeText(preview).then(() => {
145
+ copyToClipboard(preview).then(() => {
94
146
  showToast(t('profile.agents.detail.agent_config.copy'));
95
147
  });
96
148
  }
@@ -139,6 +191,7 @@ export default function TabAgentConfig({ agent, agentName, session, showToast })
139
191
  if (files.length === 0) {
140
192
  return html`
141
193
  <div>
194
+ <${ScheduleBudgetSection} agent=${agent} agentName=${agentName} showToast=${showToast} />
142
195
  <div class="pf-agd-config-upload">
143
196
  <button class="btn-outline btn-sm" onClick=${handleUploadClick}>+ ${t('profile.agents.detail.agent_config.upload')}</button>
144
197
  <input ref=${fileInputRef} type="file" accept=".md,.yaml,.yml,.json" class="pf-agd-hidden-input" onChange=${handleFileUpload} />
@@ -150,6 +203,7 @@ export default function TabAgentConfig({ agent, agentName, session, showToast })
150
203
 
151
204
  return html`
152
205
  <div>
206
+ <${ScheduleBudgetSection} agent=${agent} agentName=${agentName} showToast=${showToast} />
153
207
  <div class="pf-agd-config-upload">
154
208
  <button class="btn-outline btn-sm" onClick=${handleUploadClick}>+ ${t('profile.agents.detail.agent_config.upload')}</button>
155
209
  <input ref=${fileInputRef} type="file" accept=".md,.yaml,.yml,.json" class="pf-agd-hidden-input" onChange=${handleFileUpload} />
@@ -4,6 +4,11 @@
4
4
  * during onboarding or production status (connection, platform, readiness,
5
5
  * identity, delivery log) after completion.
6
6
  * @version-history
7
+ * v1.6.0 -- 2026-06-02 -- Component unification (#1): the 3 copy buttons (agent prompt,
8
+ * install command, curl) now use the canonical <CopyButton> with precomputed text
9
+ * payloads -- dropped 3 copied-state useState hooks, 3 hand-rolled handlers, the
10
+ * copied-flag params threaded through both render functions, and the local
11
+ * copyToClipboard import.
7
12
  * v1.5.0 -- 2026-05-31 -- Live-update refresh no longer toggles the full-tab loading
8
13
  * placeholder (added a showSpinner option). During Hello Integration the agent
9
14
  * posts steps/telemetry rapidly, so the frequent SSE ticks were re-rendering the
@@ -22,7 +27,8 @@ import { h } from 'preact';
22
27
  import { useState, useEffect, useRef } from 'preact/hooks';
23
28
  import htm from 'htm';
24
29
  import { t } from '/js/i18n.js';
25
- import { timeAgo, copyToClipboard } from '/js/utils.js';
30
+ import { timeAgo } from '/js/utils.js';
31
+ import { CopyButton } from '/components/CopyButton.js';
26
32
  import { detectAgentState } from './state-detector.js';
27
33
  import {
28
34
  getOnboarding, startOnboarding,
@@ -42,15 +48,12 @@ export default function TabIntegration({ agent, onboarding, session, showToast,
42
48
  const [loading, setLoading] = useState(true);
43
49
  const [testing, setTesting] = useState(false);
44
50
  const [rerunning, setRerunning] = useState(false);
45
- const [copiedCmd, setCopiedCmd] = useState(false);
46
- const [copiedInstall, setCopiedInstall] = useState(false);
47
51
  const [showAllDeliveries, setShowAllDeliveries] = useState(false);
48
52
  const [allDeliveries, setAllDeliveries] = useState(null);
49
53
  const [editingWebhook, setEditingWebhook] = useState(false);
50
54
  const [webhookDraft, setWebhookDraft] = useState('');
51
55
  const [savingWebhook, setSavingWebhook] = useState(false);
52
56
  const [updatingBundle, setUpdatingBundle] = useState(false);
53
- const [copiedPrompt, setCopiedPrompt] = useState(false);
54
57
 
55
58
  async function loadData({ showSpinner = true } = {}) {
56
59
  if (showSpinner) setLoading(true);
@@ -113,44 +116,23 @@ export default function TabIntegration({ agent, onboarding, session, showToast,
113
116
  setRerunning(false);
114
117
  }
115
118
 
116
- function handleCopyInstall(e) {
117
- e.stopPropagation();
118
- const url = getSkillBundleUrl(agentName);
119
- copyToClipboard(`curl -o skill-bundle.zip ${location.origin}${url}`).then(() => {
120
- setCopiedCmd(true);
121
- setTimeout(() => setCopiedCmd(false), 2000);
122
- });
123
- }
124
-
125
- function handleCopyInstallCommand(e) {
126
- e.stopPropagation();
127
- const platform = onboarding?.platformName || onboarding?.detectedPlatform || '';
128
- const url = `${location.origin}${getSkillBundleUrl(agentName)}`;
129
- let cmd;
130
- if (platform.toLowerCase().includes('hermes') || platform.toLowerCase().includes('openclaw')) {
131
- cmd = `hermes skills install ${url}`;
132
- } else {
133
- cmd = `curl -o skill-bundle.zip ${url} && unzip skill-bundle.zip`;
134
- }
135
- copyToClipboard(cmd).then(() => {
136
- setCopiedInstall(true);
137
- setTimeout(() => setCopiedInstall(false), 2000);
138
- });
139
- }
140
-
141
- function handleCopyAgentPrompt(e) {
142
- e.stopPropagation();
143
- const url = `${location.origin}${getSkillBundleUrl(agentName)}`;
144
- const prompt = `Download and install your skill bundle from AIMEAT.
119
+ // Copy-button payloads (the canonical <CopyButton> owns the copy + copied-state).
120
+ const skillBundleUrl = `${location.origin}${getSkillBundleUrl(agentName)}`;
121
+ const curlText = `curl -o skill-bundle.zip ${skillBundleUrl}`;
122
+ const installPlatform = onboarding?.platformName || onboarding?.detectedPlatform || '';
123
+ const installCmdText = (installPlatform.toLowerCase().includes('hermes') || installPlatform.toLowerCase().includes('openclaw'))
124
+ ? `hermes skills install ${skillBundleUrl}`
125
+ : `curl -o skill-bundle.zip ${skillBundleUrl} && unzip skill-bundle.zip`;
126
+ const agentPromptText = `Download and install your skill bundle from AIMEAT.
145
127
 
146
- Your skill bundle URL: ${url}
128
+ Your skill bundle URL: ${skillBundleUrl}
147
129
 
148
130
  Steps:
149
131
  1. Authenticate with your agent token: POST ${location.origin}/v1/auth/token
150
132
  Sign the challenge with your private key to get a JWT.
151
133
 
152
134
  2. Download the skill bundle ZIP:
153
- GET ${url}
135
+ GET ${skillBundleUrl}
154
136
  Header: Authorization: Bearer <your-jwt>
155
137
  Save the response as a ZIP file.
156
138
 
@@ -162,12 +144,7 @@ Steps:
162
144
  4. Read SKILL.md first -- it has your personalized directives, rules, and all the API endpoints you need.
163
145
 
164
146
  If you already have a JWT token, you can do this in one step:
165
- curl -H "Authorization: Bearer <jwt>" -o skill-bundle.zip "${url}" && unzip skill-bundle.zip && cat */SKILL.md`;
166
- copyToClipboard(prompt).then(() => {
167
- setCopiedPrompt(true);
168
- setTimeout(() => setCopiedPrompt(false), 3000);
169
- });
170
- }
147
+ curl -H "Authorization: Bearer <jwt>" -o skill-bundle.zip "${skillBundleUrl}" && unzip skill-bundle.zip && cat */SKILL.md`;
171
148
 
172
149
  function handleEditWebhook(e) {
173
150
  e.stopPropagation();
@@ -212,7 +189,7 @@ curl -H "Authorization: Bearer <jwt>" -o skill-bundle.zip "${url}" && unzip skil
212
189
  const isOnboarding = state === 'new' || state === 'onboarding';
213
190
 
214
191
  if (isOnboarding) {
215
- return renderOnboardingView(onboarding, agentName, handleRerun, rerunning, handleCopyInstall, copiedCmd, handleCopyInstallCommand, copiedInstall, handleCopyAgentPrompt, copiedPrompt);
192
+ return renderOnboardingView(onboarding, agentName, handleRerun, rerunning, curlText, installCmdText, agentPromptText);
216
193
  }
217
194
 
218
195
  async function handleShowAll() {
@@ -226,10 +203,10 @@ curl -H "Authorization: Bearer <jwt>" -o skill-bundle.zip "${url}" && unzip skil
226
203
 
227
204
  const displayDeliveries = showAllDeliveries && allDeliveries ? allDeliveries : deliveries;
228
205
 
229
- return renderProductionView(agent, onboarding, webhook, bundleVersion, displayDeliveries, handleTestWebhook, testing, handleRerun, rerunning, handleCopyInstall, copiedCmd, handleShowAll, showAllDeliveries, editingWebhook, webhookDraft, setWebhookDraft, handleEditWebhook, handleSaveWebhook, handleCancelWebhook, savingWebhook, handleUpdateBundle, updatingBundle, handleCopyInstallCommand, copiedInstall, handleCopyAgentPrompt, copiedPrompt, postChecklist);
206
+ return renderProductionView(agent, onboarding, webhook, bundleVersion, displayDeliveries, handleTestWebhook, testing, handleRerun, rerunning, curlText, handleShowAll, showAllDeliveries, editingWebhook, webhookDraft, setWebhookDraft, handleEditWebhook, handleSaveWebhook, handleCancelWebhook, savingWebhook, handleUpdateBundle, updatingBundle, agentPromptText, postChecklist);
230
207
  }
231
208
 
232
- function renderOnboardingView(onboarding, agentName, handleRerun, rerunning, handleCopyInstall, copiedCmd, handleCopyInstallCommand, copiedInstall, handleCopyAgentPrompt, copiedPrompt) {
209
+ function renderOnboardingView(onboarding, agentName, handleRerun, rerunning, curlText, installCmdText, agentPromptText) {
233
210
  const steps = onboarding?.steps || [];
234
211
  const passed = steps.filter(s => s.status === 'passed').length;
235
212
  const total = steps.length || 11;
@@ -269,15 +246,15 @@ function renderOnboardingView(onboarding, agentName, handleRerun, rerunning, han
269
246
  <div class="pf-agd-section">
270
247
  <div class="pf-agd-section-label">${t('profile.agents.skillBundle.title')}</div>
271
248
  <div class="pf-agd-form-actions">
272
- <button class="btn-primary btn-sm" onClick=${handleCopyAgentPrompt}>
273
- ${copiedPrompt ? '✓ ' + t('profile.agents.detail.integration.promptCopied') : t('profile.agents.detail.integration.copyAgentPrompt')}
274
- </button>
275
- <button class="btn-outline btn-sm" onClick=${handleCopyInstallCommand}>
276
- ${copiedInstall ? '✓ ' + t('profile.agents.detail.integration.installCommandCopied') : t('profile.agents.detail.integration.copyInstallCommand')}
277
- </button>
278
- <button class="btn-outline btn-sm" onClick=${handleCopyInstall}>
279
- ${copiedCmd ? '✓ ' + t('profile.agents.copied') : t('profile.agents.skillBundle.copyCurl')}
280
- </button>
249
+ <${CopyButton} text=${agentPromptText} className="btn-primary btn-sm"
250
+ label=${t('profile.agents.detail.integration.copyAgentPrompt')}
251
+ copiedLabel=${'✓ ' + t('profile.agents.detail.integration.promptCopied')} />
252
+ <${CopyButton} text=${installCmdText} className="btn-outline btn-sm"
253
+ label=${t('profile.agents.detail.integration.copyInstallCommand')}
254
+ copiedLabel=${'✓ ' + t('profile.agents.detail.integration.installCommandCopied')} />
255
+ <${CopyButton} text=${curlText} className="btn-outline btn-sm"
256
+ label=${t('profile.agents.skillBundle.copyCurl')}
257
+ copiedLabel=${'✓ ' + t('profile.agents.copied')} />
281
258
  <a class="btn-outline btn-sm" href=${getSkillBundleUrl(agentName)} download>
282
259
  ${t('profile.agents.skillBundle.downloadZip')}
283
260
  </a>
@@ -293,7 +270,7 @@ function renderOnboardingView(onboarding, agentName, handleRerun, rerunning, han
293
270
  `;
294
271
  }
295
272
 
296
- function renderProductionView(agent, onboarding, webhook, bundleVersion, displayDeliveries, handleTestWebhook, testing, handleRerun, rerunning, handleCopyInstall, copiedCmd, handleShowAll, showAllDeliveries, editingWebhook, webhookDraft, setWebhookDraft, handleEditWebhook, handleSaveWebhook, handleCancelWebhook, savingWebhook, handleUpdateBundle, updatingBundle, handleCopyInstallCommand, copiedInstall, handleCopyAgentPrompt, copiedPrompt, postChecklist) {
273
+ function renderProductionView(agent, onboarding, webhook, bundleVersion, displayDeliveries, handleTestWebhook, testing, handleRerun, rerunning, curlText, handleShowAll, showAllDeliveries, editingWebhook, webhookDraft, setWebhookDraft, handleEditWebhook, handleSaveWebhook, handleCancelWebhook, savingWebhook, handleUpdateBundle, updatingBundle, agentPromptText, postChecklist) {
297
274
  const steps = onboarding?.steps || [];
298
275
  const agentName = agent.name;
299
276
 
@@ -397,12 +374,12 @@ function renderProductionView(agent, onboarding, webhook, bundleVersion, display
397
374
  </div>
398
375
  ` : ''}
399
376
  <div class="pf-agd-form-actions">
400
- <button class="btn-primary btn-sm" onClick=${handleCopyAgentPrompt}>
401
- ${copiedPrompt ? '✓ ' + t('profile.agents.detail.integration.promptCopied') : t('profile.agents.detail.integration.copyAgentPrompt')}
402
- </button>
403
- <button class="btn-outline btn-sm" onClick=${handleCopyInstall}>
404
- ${copiedCmd ? '✓ ' + t('profile.agents.copied') : t('profile.agents.skillBundle.reinstall')}
405
- </button>
377
+ <${CopyButton} text=${agentPromptText} className="btn-primary btn-sm"
378
+ label=${t('profile.agents.detail.integration.copyAgentPrompt')}
379
+ copiedLabel=${'✓ ' + t('profile.agents.detail.integration.promptCopied')} />
380
+ <${CopyButton} text=${curlText} className="btn-outline btn-sm"
381
+ label=${t('profile.agents.skillBundle.reinstall')}
382
+ copiedLabel=${'✓ ' + t('profile.agents.copied')} />
406
383
  <button class="btn-outline btn-sm" onClick=${handleUpdateBundle} disabled=${updatingBundle}>
407
384
  ${updatingBundle ? '...' : t('profile.agents.detail.integration.update')}
408
385
  </button>
@@ -3,6 +3,9 @@
3
3
  * @description Messages tab with command palette, "/" autocomplete, and chat area.
4
4
  * Wraps the existing messages subtab and adds command discovery.
5
5
  * @version-history
6
+ * v1.4.0 -- 2026-06-02 -- Render agent (outbound) message bodies as Markdown via the
7
+ * shared safe vnode Markdown component, so LLM replies (bold, lists, code,
8
+ * tables) display formatted. Owner inbound messages stay literal text.
6
9
  * v1.3.0 -- 2026-05-30 -- Render agent single-select option-prompts (metadata.prompt) as
7
10
  * clickable chips + an implicit "Other"; clicking sends a correlated prompt_answer. A
8
11
  * prompt locks (read-only, chosen chip highlighted) once a newer message exists.
@@ -18,6 +21,7 @@ import { t } from '/js/i18n.js';
18
21
  import { timeAgo } from '/js/utils.js';
19
22
  import { sendMessage, listMessages, listThreads } from '/js/services/agent-messages.js';
20
23
  import { getAgentCommands } from '/js/services/agent-integration.js';
24
+ import { Markdown } from '/components/Markdown.js';
21
25
 
22
26
  const html = htm.bind(h);
23
27
 
@@ -222,7 +226,12 @@ export default function TabMessages({ agent, agentName, session, showToast }) {
222
226
  <div key=${msg.id || msg.createdAt}>
223
227
  <div class="pf-agd-msg-bubble ${msg.direction === 'inbound' ? 'pf-agd-msg-inbound' : 'pf-agd-msg-outbound'} ${isCommand ? 'pf-agd-msg-command' : ''} ${isReply ? 'pf-agd-msg-reply' : ''}">
224
228
  ${isCommand && html`<span class="pf-agd-command-badge">${t('profile.agents.detail.messages.command')}</span>`}
225
- ${msg.content}
229
+ ${msg.direction === 'outbound'
230
+ // Agent replies are markdown (LLM output). Render them safely via the
231
+ // shared vnode Markdown component. Owner-typed inbound messages stay
232
+ // literal — the input is a plain text field, not markdown.
233
+ ? html`<${Markdown} text=${msg.content || ''} />`
234
+ : msg.content}
226
235
  </div>
227
236
  <div class="pf-agd-msg-meta ${msg.direction === 'inbound' ? 'pf-agd-msg-meta-right' : ''}">
228
237
  ${msg.createdAt ? html`<span class="pf-agd-msg-time">${new Date(msg.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</span> ${timeAgo(msg.createdAt)}` : ''}
@@ -10,6 +10,9 @@
10
10
  * @structure default export TabQuality({ agentName })
11
11
  * @usage rendered by agent-card.js renderTabContent() for the 'quality' tab
12
12
  * @version-history
13
+ * v1.2.0 -- 2026-06-01 -- Stop the empty flash on refresh: only show the loading
14
+ * state on first load; live-update refetches swap data in place (and keep the
15
+ * last good data on a transient error) instead of blanking the tab.
13
16
  * v1.1.0 -- 2026-05-31 -- Add "Rate deliverables" list: completed tasks the owner
14
17
  * can rate inline via the shared RateModal (unrated first); refreshes the
15
18
  * rollups on submit.
@@ -50,15 +53,18 @@ export default function TabQuality({ agentName, showToast }) {
50
53
  const [sendingRate, setSendingRate] = useState(false);
51
54
 
52
55
  async function loadData() {
53
- setLoading(true);
56
+ // NB: do NOT blank `data`/`loading` here. The initial render shows the
57
+ // loading state (loading starts true); every later call -- including
58
+ // live-update refetches -- swaps in fresh data in place without flashing an
59
+ // empty tab. A transient error keeps the last good data rather than clearing.
54
60
  try {
55
61
  const [statsResp, tasksResp] = await Promise.all([
56
62
  getAgentStatistics(agentName),
57
63
  listTasks(agentName, { status: 'done', per_page: 100 }).catch(() => null),
58
64
  ]);
59
- setData(statsResp?.data || null);
65
+ if (statsResp?.data) setData(statsResp.data);
60
66
  setDoneTasks(tasksResp?.data?.tasks || []);
61
- } catch { setData(null); }
67
+ } catch { /* keep showing the last good data on a transient error */ }
62
68
  setLoading(false);
63
69
  }
64
70
 
@@ -104,26 +110,26 @@ export default function TabQuality({ agentName, showToast }) {
104
110
  <!-- Performance -->
105
111
  <div class="pf-agd-section-title">${t('profile.agents.detail.quality.performanceTitle')}</div>
106
112
  <div class="pf-agd-quality-desc">${t('profile.agents.detail.quality.performanceDesc')}</div>
107
- <div class="pf-agd-stats-grid">
108
- <div class="pf-agd-stat-card">
109
- <div class="pf-agd-stat-value">${perf.tasks?.total ?? 0}</div>
110
- <div class="pf-agd-stat-label">${t('profile.agents.detail.quality.tasksTotal')}</div>
113
+ <div class="stat-grid">
114
+ <div class="stat-card">
115
+ <div class="stat-card-value">${perf.tasks?.total ?? 0}</div>
116
+ <div class="stat-card-label">${t('profile.agents.detail.quality.tasksTotal')}</div>
111
117
  </div>
112
- <div class="pf-agd-stat-card">
113
- <div class="pf-agd-stat-value">${perf.tasks?.completed ?? 0}</div>
114
- <div class="pf-agd-stat-label">${t('profile.agents.detail.quality.tasksCompleted')}</div>
118
+ <div class="stat-card">
119
+ <div class="stat-card-value">${perf.tasks?.completed ?? 0}</div>
120
+ <div class="stat-card-label">${t('profile.agents.detail.quality.tasksCompleted')}</div>
115
121
  </div>
116
- <div class="pf-agd-stat-card">
117
- <div class="pf-agd-stat-value">${perf.tasks?.successRate != null ? `${Math.round(perf.tasks.successRate * 100)}%` : '-'}</div>
118
- <div class="pf-agd-stat-label">${t('profile.agents.detail.quality.successRate')}</div>
122
+ <div class="stat-card">
123
+ <div class="stat-card-value">${perf.tasks?.successRate != null ? `${Math.round(perf.tasks.successRate * 100)}%` : '-'}</div>
124
+ <div class="stat-card-label">${t('profile.agents.detail.quality.successRate')}</div>
119
125
  </div>
120
- <div class="pf-agd-stat-card">
121
- <div class="pf-agd-stat-value">${fmtSeconds(perf.duration?.avgCompletionSeconds)}</div>
122
- <div class="pf-agd-stat-label">${t('profile.agents.detail.quality.avgTime')}</div>
126
+ <div class="stat-card">
127
+ <div class="stat-card-value">${fmtSeconds(perf.duration?.avgCompletionSeconds)}</div>
128
+ <div class="stat-card-label">${t('profile.agents.detail.quality.avgTime')}</div>
123
129
  </div>
124
- <div class="pf-agd-stat-card">
125
- <div class="pf-agd-stat-value">${perf.events?.total ?? 0}</div>
126
- <div class="pf-agd-stat-label">${t('profile.agents.detail.quality.events')}</div>
130
+ <div class="stat-card">
131
+ <div class="stat-card-value">${perf.events?.total ?? 0}</div>
132
+ <div class="stat-card-label">${t('profile.agents.detail.quality.events')}</div>
127
133
  </div>
128
134
  </div>
129
135
  ${durKeys.length > 0 && html`
@@ -0,0 +1,110 @@
1
+ /**
2
+ * @file tab-schedules.js
3
+ * @description Agent detail › Schedules sub-tab. Shows this agent's schedules in
4
+ * two groups: AIMEAT-dispatched (server-managed; full controls) and Agent
5
+ * internal (the agent's self-reported mirror, read-only). Lets the owner create
6
+ * a new schedule targeting this agent (reusing the master view's CreateForm).
7
+ * @version-history
8
+ * v1.0.0 -- 2026-06-03 -- Initial agent Schedules sub-tab
9
+ */
10
+ import { h } from 'preact';
11
+ import { useState, useEffect, useRef, useCallback } from 'preact/hooks';
12
+ import htm from 'htm';
13
+ import { t } from '/js/i18n.js';
14
+ import { timeAgo } from '/js/utils.js';
15
+ import { listAgentSchedules, setScheduleEnabled, triggerSchedule, deleteSchedule } from '/js/services/schedules.js';
16
+ import { CreateForm } from '../scheduler-tab.js';
17
+
18
+ const html = htm.bind(h);
19
+
20
+ const KIND_LABEL = { ai: 'profile.scheduler.kind.ai', agent_task: 'profile.scheduler.kind.agent_task', extension: 'profile.scheduler.kind.extension', core: 'profile.scheduler.kind.core' };
21
+
22
+ export default function TabSchedules({ agentName, allAgents = [], showToast }) {
23
+ const [managed, setManaged] = useState([]);
24
+ const [internal, setInternal] = useState([]);
25
+ const [loading, setLoading] = useState(true);
26
+ const [showForm, setShowForm] = useState(false);
27
+
28
+ const loadData = useCallback(async () => {
29
+ try {
30
+ const res = await listAgentSchedules(agentName);
31
+ setManaged(res?.data?.managed || []);
32
+ setInternal(res?.data?.agentInternal || []);
33
+ } catch (e) {
34
+ showToast?.(e.message, true);
35
+ } finally { setLoading(false); }
36
+ }, [agentName, showToast]);
37
+
38
+ const loadRef = useRef(loadData);
39
+ loadRef.current = loadData;
40
+ useEffect(() => {
41
+ loadData();
42
+ const handler = () => loadRef.current();
43
+ window.addEventListener('aimeat-live-update', handler);
44
+ const poller = setInterval(() => loadRef.current(), 15000);
45
+ return () => { window.removeEventListener('aimeat-live-update', handler); clearInterval(poller); };
46
+ }, [loadData]);
47
+
48
+ const onToggle = async (j) => { try { await setScheduleEnabled(j.id, !j.enabled); await loadData(); } catch (e) { showToast?.(e.message, true); } };
49
+ const onTrigger = async (j) => { try { await triggerSchedule(j.id); showToast?.(t('profile.scheduler.triggered')); await loadData(); } catch (e) { showToast?.(e.message, true); } };
50
+ const onCancel = async (j) => {
51
+ if (!window.confirm(t('profile.scheduler.confirmCancel'))) return;
52
+ try { await deleteSchedule(j.id); showToast?.(t('profile.scheduler.cancelled')); await loadData(); } catch (e) { showToast?.(e.message, true); }
53
+ };
54
+
55
+ if (loading) return html`<div class="sch-loading">${t('profile.scheduler.loading')}</div>`;
56
+
57
+ return html`
58
+ <div class="sch-tab">
59
+ <div class="sch-head">
60
+ <div class="section-desc">${t('profile.scheduler.agentDesc')}</div>
61
+ <button class="btn-primary" onClick=${() => setShowForm(v => !v)}>
62
+ ${showForm ? t('profile.scheduler.close') : t('profile.scheduler.newSchedule')}
63
+ </button>
64
+ </div>
65
+
66
+ ${showForm && html`<${CreateForm} agents=${allAgents} lockedAgent=${agentName} showToast=${showToast}
67
+ onCreated=${() => { setShowForm(false); loadData(); }} />`}
68
+
69
+ <div class="sch-section">
70
+ <h3 class="sch-section-title">${t('profile.scheduler.dispatchedTitle')}</h3>
71
+ ${managed.length === 0
72
+ ? html`<div class="sch-empty">${t('profile.scheduler.noDispatched')}</div>`
73
+ : html`<table class="sch-table"><thead><tr>
74
+ <th>${t('profile.scheduler.col.name')}</th>
75
+ <th>${t('profile.scheduler.col.kind')}</th>
76
+ <th>${t('profile.scheduler.col.cron')}</th>
77
+ <th>${t('profile.scheduler.col.lastRun')}</th>
78
+ <th>${t('profile.scheduler.col.nextRun')}</th>
79
+ <th></th>
80
+ </tr></thead><tbody>
81
+ ${managed.map(j => html`<tr key=${j.id}>
82
+ <td><div class="sch-name">${j.displayName || j.name}</div>${j.createdByAgent && html`<span class="sch-tag">${t('profile.scheduler.byAgent')}</span>`}</td>
83
+ <td><span class="sch-badge sch-badge--${j.type === 'ai' ? 'ai' : j.type === 'extension' ? 'ext' : 'agent'}">${t(KIND_LABEL[j.type] || KIND_LABEL.core)}</span></td>
84
+ <td><code class="sch-cron">${j.cron}</code>${j.timezone && html`<div class="sch-muted">${j.timezone}</div>`}</td>
85
+ <td>${j.lastRunAt ? timeAgo(j.lastRunAt) : html`<span class="sch-muted">${t('profile.scheduler.never')}</span>`} ${j.lastRunResult ? html`<span class="sch-badge ${j.lastRunResult === 'error' ? 'sch-badge--err' : 'sch-badge--ok'}">${j.lastRunResult}</span>` : ''}</td>
86
+ <td>${j.nextRunAt ? timeAgo(j.nextRunAt) : html`<span class="sch-muted">—</span>`}</td>
87
+ <td class="sch-actions">
88
+ <button class="btn-ghost sch-btn" onClick=${() => onToggle(j)}>${j.enabled ? t('profile.scheduler.pause') : t('profile.scheduler.resume')}</button>
89
+ <button class="btn-ghost sch-btn" onClick=${() => onTrigger(j)}>${t('profile.scheduler.runNow')}</button>
90
+ <button class="btn-danger sch-btn" onClick=${() => onCancel(j)}>${t('profile.scheduler.cancel')}</button>
91
+ </td>
92
+ </tr>`)}
93
+ </tbody></table>`}
94
+ </div>
95
+
96
+ <div class="sch-section">
97
+ <h3 class="sch-section-title">${t('profile.scheduler.internalTitle')}</h3>
98
+ <div class="sch-muted sch-internal-note">${t('profile.scheduler.internalNote')}</div>
99
+ ${internal.length === 0
100
+ ? html`<div class="sch-empty">${t('profile.scheduler.noInternal')}</div>`
101
+ : html`<table class="sch-table"><tbody>
102
+ ${internal.map((e, i) => html`<tr key=${e.id || i}>
103
+ <td>${e.name}${e.purpose && html`<div class="sch-muted">${e.purpose}</div>`}</td>
104
+ <td><code class="sch-cron">${e.cron || e.schedule || '—'}</code>${e.timezone && html`<div class="sch-muted">${e.timezone}</div>`}</td>
105
+ <td>${e.status || 'active'}</td>
106
+ </tr>`)}
107
+ </tbody></table>`}
108
+ </div>
109
+ </div>`;
110
+ }
@@ -35,11 +35,11 @@ function StatsCards({ stats }) {
35
35
  ];
36
36
 
37
37
  return html`
38
- <div class="agd-stats-grid">
38
+ <div class="stat-grid">
39
39
  ${cards.map(c => html`
40
- <div class="agd-stat-card" key=${c.key}>
41
- <div class="agd-stat-value">${c.value}</div>
42
- <div class="agd-stat-label">${t(`profile.agents.activity.${c.key}`)}</div>
40
+ <div class="stat-card" key=${c.key}>
41
+ <div class="stat-card-value">${c.value}</div>
42
+ <div class="stat-card-label">${t(`profile.agents.activity.${c.key}`)}</div>
43
43
  </div>
44
44
  `)}
45
45
  </div>
@@ -55,13 +55,13 @@ export default function AgentServicesSubtab({ agentName, session, showToast }) {
55
55
  const [services, setServices] = useState([]);
56
56
  const [loading, setLoading] = useState(true);
57
57
 
58
- async function loadServices() {
59
- setLoading(true);
58
+ async function loadServices({ showSpinner = true } = {}) {
59
+ if (showSpinner) setLoading(true);
60
60
  try {
61
61
  const res = await getAgentServices(agentName);
62
62
  setServices(res.actions || []);
63
63
  } catch (err) {
64
- showToast(err.message || t('profile.agents.services.loadError'), true);
64
+ if (showSpinner) showToast(err.message || t('profile.agents.services.loadError'), true);
65
65
  }
66
66
  setLoading(false);
67
67
  }
@@ -88,7 +88,8 @@ export default function AgentServicesSubtab({ agentName, session, showToast }) {
88
88
  useEffect(() => { loadServices(); }, [agentName]);
89
89
 
90
90
  useEffect(() => {
91
- const handler = () => { loadServices(); };
91
+ // Silent refetch on live-update so the tab doesn't flash blank.
92
+ const handler = () => { loadServices({ showSpinner: false }); };
92
93
  window.addEventListener('aimeat-live-update', handler);
93
94
  return () => window.removeEventListener('aimeat-live-update', handler);
94
95
  }, [agentName]);