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
@@ -1,13 +1,19 @@
1
1
  /**
2
- * Portal (Genesis 001) — View Module
3
- * Main landing page with 3D canvas, live feed, mega-prompts, and expandable world.
2
+ * @file portal.js
3
+ * @description Portal (Genesis 001) view module — main landing page with 3D
4
+ * canvas, live feed, mega-prompts, and expandable world.
5
+ * @version-history
6
+ * v1.1.0 — 2026-06-02 — Component unification (#11): live-feed header dot uses
7
+ * canonical <StatusDot status="live" /> instead of bespoke .live-dot.
4
8
  */
5
9
  import { h } from 'preact';
6
10
  import { useState, useEffect, useRef, useCallback } from 'preact/hooks';
7
11
  import htm from 'htm';
8
12
  import { t } from '/js/i18n.js';
9
- import { escHtml, copyToClipboard } from '/js/utils.js';
13
+ import { escHtml } from '/js/utils.js';
14
+ import { CopyButton as BaseCopyButton } from '/components/CopyButton.js';
10
15
  import { useViewCSS } from '/components/useViewCSS.js';
16
+ import { StatusDot } from '/components/StatusDot.js';
11
17
 
12
18
  const html = htm.bind(h);
13
19
  const NODE_URL = window.location.origin;
@@ -990,7 +996,7 @@ function OnelinersFeed({ locale }) {
990
996
  return html`
991
997
  <div class="content-group">
992
998
  <div class="oneliners-header">
993
- <div class="live-dot"></div>
999
+ <${StatusDot} status="live" />
994
1000
  <span class="oneliners-label">${t('portal.oneliners.title')}</span>
995
1001
  </div>
996
1002
  <section class="provocation">
@@ -1016,21 +1022,10 @@ function OnelinersFeed({ locale }) {
1016
1022
  /* ══════════════════════════════════════════════
1017
1023
  COPY BUTTON COMPONENT
1018
1024
  ══════════════════════════════════════════════ */
1025
+ // Thin wrapper over the canonical CopyButton (aliased to avoid the name clash) \u2014
1026
+ // keeps the .copy-btn styling + the "<copied> \u2714" confirmed label.
1019
1027
  function CopyButton({ text, label, copiedLabel }) {
1020
- const [copied, setCopied] = useState(false);
1021
-
1022
- const handleCopy = useCallback(() => {
1023
- copyToClipboard(text).then(() => {
1024
- setCopied(true);
1025
- setTimeout(() => setCopied(false), 2000);
1026
- });
1027
- }, [text]);
1028
-
1029
- return html`
1030
- <button class="copy-btn ${copied ? 'copied' : ''}" type="button" onClick=${handleCopy}>
1031
- ${copied ? (copiedLabel || t('portal.prompt.copied')) + ' \u2714' : label}
1032
- </button>
1033
- `;
1028
+ return html`<${BaseCopyButton} text=${text} label=${label} copiedLabel=${(copiedLabel || t('portal.prompt.copied')) + ' \u2714'} className="copy-btn" />`;
1034
1029
  }
1035
1030
 
1036
1031
  /* ══════════════════════════════════════════════
@@ -1120,9 +1115,9 @@ function UserWorldAccordion({ navigate }) {
1120
1115
  </div>
1121
1116
  <div class="user-world-grid">
1122
1117
  ${statItems.map(([key, val]) => html`
1123
- <div class="user-stat" key=${key}>
1124
- <div class="user-stat-label">${t('portal.userWorld.stats.' + key)}</div>
1125
- <div class="user-stat-value">${val}</div>
1118
+ <div class="stat-card" key=${key}>
1119
+ <div class="stat-card-label">${t('portal.userWorld.stats.' + key)}</div>
1120
+ <div class="stat-card-value">${val}</div>
1126
1121
  </div>
1127
1122
  `)}
1128
1123
  </div>
@@ -1,11 +1,19 @@
1
+ /**
2
+ * @file portfolio.js
3
+ * @description Portfolio view — builder (select content, generate AI prompt,
4
+ * upload HTML) and public viewer (sandboxed iframe render).
5
+ * @version-history
6
+ * v1.1.0 — 2026-06-02 — Migrate bespoke prompt-copy button to canonical CopyButton component
7
+ */
1
8
  import { h } from 'preact';
2
9
  import { useState, useEffect, useCallback, useRef } from 'preact/hooks';
3
10
  import htm from 'htm';
4
11
  const html = htm.bind(h);
5
12
  import { t } from '/js/i18n.js';
6
- import { escHtml, copyToClipboard, handleImgError } from '/js/utils.js';
13
+ import { escHtml, handleImgError } from '/js/utils.js';
7
14
  import { apiGet, apiPut } from '/js/api.js';
8
15
  import TagCloud from '/js/components/tag-cloud.js';
16
+ import { CopyButton } from '/components/CopyButton.js';
9
17
 
10
18
  const NODE_URL = typeof window !== 'undefined' ? window.location.origin : '';
11
19
 
@@ -238,7 +246,6 @@ function PortfolioBuilder({ session, navigate }) {
238
246
 
239
247
  // Prompt
240
248
  const [generatedPrompt, setGeneratedPrompt] = useState('');
241
- const [promptCopied, setPromptCopied] = useState(false);
242
249
 
243
250
  // Upload
244
251
  const [uploading, setUploading] = useState(false);
@@ -313,13 +320,6 @@ function PortfolioBuilder({ session, navigate }) {
313
320
  });
314
321
  };
315
322
 
316
- // Copy prompt
317
- const handleCopyPrompt = async () => {
318
- await copyToClipboard(generatedPrompt);
319
- setPromptCopied(true);
320
- setTimeout(() => setPromptCopied(false), 2000);
321
- };
322
-
323
323
  // Download prompt as .txt
324
324
  const handleDownloadPrompt = () => {
325
325
  const blob = new Blob([generatedPrompt], { type: 'text/plain' });
@@ -614,9 +614,8 @@ function PortfolioBuilder({ session, navigate }) {
614
614
  ${generatedPrompt && html`
615
615
  <div class="portfolio-prompt-output">${generatedPrompt}</div>
616
616
  <div class="portfolio-prompt-actions">
617
- <button class="btn btn-primary" onClick=${handleCopyPrompt}>
618
- ${promptCopied ? t('portfolio.builder.promptCopied') : t('portfolio.builder.copyPrompt')}
619
- </button>
617
+ <${CopyButton} text=${generatedPrompt} className="btn-primary"
618
+ label=${t('portfolio.builder.copyPrompt')} copiedLabel=${t('portfolio.builder.promptCopied')} />
620
619
  <button class="btn btn-ghost" onClick=${handleDownloadPrompt}>
621
620
  ${t('portfolio.builder.downloadPrompt')}
622
621
  </button>
@@ -22,6 +22,7 @@ import { escHtml, copyToClipboard } from '/js/utils.js';
22
22
  import { getNodeUrl } from '/js/services/auth.js';
23
23
  import { apiGet, apiPost, apiDelete } from '/js/api.js';
24
24
  import { useConfirm } from '/components/Modal.js';
25
+ import { CopyButton } from '/components/CopyButton.js';
25
26
  import * as groupsApi from '/js/services/sharing-groups.js';
26
27
  import { getOwnerDefaults, upsertOwnerDefaults } from '/js/services/agent-directives.js';
27
28
 
@@ -532,6 +533,247 @@ function AgentDefaultsSection({ showToast }) {
532
533
  `;
533
534
  }
534
535
 
536
+ /* ═══════════════════════════════════════════════════════════════════
537
+ Agent Access Tokens Section
538
+ ═══════════════════════════════════════════════════════════════════ */
539
+
540
+ // Mirrors the agent scope domains (agents-tab.js) — the same list agents are granted from.
541
+ const PAT_SCOPE_DOMAINS = [
542
+ { key: 'memory', permissions: ['read', 'write', 'delete'] },
543
+ { key: 'work', permissions: ['request', 'read', 'accept', 'publish'] },
544
+ { key: 'social', permissions: ['read', 'write'] },
545
+ { key: 'wallet', permissions: ['read'] },
546
+ { key: 'consent', permissions: ['manage'] },
547
+ { key: 'catalogue', permissions: ['read'] },
548
+ { key: 'generator', permissions: ['read', 'write', 'execute'] },
549
+ { key: 'task', permissions: ['read', 'write', 'manage'] },
550
+ { key: 'cortex', permissions: ['write'] },
551
+ { key: 'ext', permissions: ['write'] },
552
+ ];
553
+
554
+ function buildPatAgentPrompt(tokenData) {
555
+ const url = (typeof window !== 'undefined' ? window.location.origin : '');
556
+ const level = tokenData.grant_operator ? 'operator (full node control)'
557
+ : tokenData.grant_owner ? 'owner (acts as me)'
558
+ : `scoped agent (${(tokenData.scopes || []).join(', ') || 'no scopes'})`;
559
+ return `I'm giving you an AIMEAT access token so you can log in to my node (${url}) as ${level} and check that a web app works.
560
+
561
+ Use it by adding this HTTP header to your requests (and, for browser testing, set it as an extra header in your browser automation so every request the page makes carries it):
562
+
563
+ Authorization: Bearer ${tokenData.token}
564
+
565
+ The server recognizes the token and treats every request as if I'm logged in — no extra login step. Open the app, click through it, read/write via it, and confirm it behaves correctly. Tell me what you find.`;
566
+ }
567
+
568
+ function AccessTokensSection({ session, showToast }) {
569
+ const { confirm, ConfirmUI } = useConfirm();
570
+ const isOperator = (session.roles || []).includes('operator');
571
+
572
+ const [tokens, setTokens] = useState(null);
573
+ const [showCreate, setShowCreate] = useState(false);
574
+ const [creating, setCreating] = useState(false);
575
+ const [created, setCreated] = useState(null); // { token, prompt }
576
+
577
+ // Create form
578
+ const [label, setLabel] = useState('');
579
+ const [selScopes, setSelScopes] = useState({});
580
+ const [grantOwner, setGrantOwner] = useState(false);
581
+ const [grantOperator, setGrantOperator] = useState(false);
582
+ const [readOwnerData, setReadOwnerData] = useState(false);
583
+ const [expiry, setExpiry] = useState('');
584
+
585
+ const fullLevel = grantOwner || grantOperator;
586
+
587
+ const load = useCallback(async () => {
588
+ try { const r = await apiGet('/v1/access/tokens'); setTokens(r.data?.tokens || []); }
589
+ catch { setTokens([]); }
590
+ }, []);
591
+ useEffect(() => { load(); }, [load]);
592
+
593
+ const liveRef = useRef(load);
594
+ liveRef.current = load;
595
+ useEffect(() => {
596
+ const handler = () => liveRef.current();
597
+ window.addEventListener('aimeat-live-update', handler);
598
+ return () => window.removeEventListener('aimeat-live-update', handler);
599
+ }, []);
600
+
601
+ const resetForm = () => {
602
+ setShowCreate(false); setLabel(''); setSelScopes({});
603
+ setGrantOwner(false); setGrantOperator(false); setReadOwnerData(false); setExpiry('');
604
+ };
605
+
606
+ const toggleScope = (s) => setSelScopes(prev => ({ ...prev, [s]: !prev[s] }));
607
+
608
+ const handleCreate = useCallback(async () => {
609
+ if (!label.trim()) { showToast(t('profile.access.patLabelRequired') || 'Give the token a name'); return; }
610
+ const scopes = fullLevel ? [] : Object.keys(selScopes).filter(k => selScopes[k]);
611
+ if (!fullLevel && scopes.length === 0) {
612
+ showToast(t('profile.access.patScopeRequired') || 'Select at least one scope, or Full / Operator');
613
+ return;
614
+ }
615
+ setCreating(true);
616
+ try {
617
+ const body = {
618
+ label: label.trim(), scopes,
619
+ grant_owner: grantOwner, grant_operator: grantOperator, read_owner_data: readOwnerData,
620
+ };
621
+ if (expiry) body.expires_in = parseInt(expiry, 10);
622
+ const r = await apiPost('/v1/access/tokens', body);
623
+ const data = r.data;
624
+ setCreated({ token: data.token, prompt: buildPatAgentPrompt(data) });
625
+ resetForm();
626
+ load();
627
+ } catch (e) {
628
+ showToast(e.message || (t('profile.access.patCreateError') || 'Failed to create token'));
629
+ } finally { setCreating(false); }
630
+ }, [label, fullLevel, selScopes, grantOwner, grantOperator, readOwnerData, expiry, showToast, load]);
631
+
632
+ const handleRevoke = useCallback((id, lbl) => {
633
+ confirm(
634
+ (t('profile.access.patConfirmRevoke') || 'Revoke token "{name}"? Agents using it lose access immediately.').replace('{name}', lbl),
635
+ async () => {
636
+ try {
637
+ await apiDelete('/v1/access/tokens/' + id);
638
+ showToast(t('profile.access.patRevoked') || 'Token revoked');
639
+ load();
640
+ } catch (e) { showToast(e.message); }
641
+ },
642
+ { danger: true },
643
+ );
644
+ }, [confirm, showToast, load]);
645
+
646
+ const levelBadge = (tok) => {
647
+ if (tok.grant_operator) return html`<span class="badge badge-danger">${t('profile.access.patOperator') || 'Operator'}</span>`;
648
+ if (tok.grant_owner) return html`<span class="badge badge-warn">${t('profile.access.patFullOwner') || 'Full owner'}</span>`;
649
+ return html`<span class="badge badge-info">${(tok.scopes || []).length} ${t('profile.access.patScopes') || 'scopes'}</span>`;
650
+ };
651
+
652
+ return html`
653
+ <h3 class="card-h3 mt-section">\u{1F511} ${t('profile.access.patTitle') || 'Agent Access Tokens'}</h3>
654
+ <div class="section-desc">${t('profile.access.patDesc') || 'Create a revocable token an agent can use (as a Bearer header) to log in and test your apps. One token can be shared across all your agents.'}</div>
655
+
656
+ ${created && html`
657
+ <div class="card access-card-warn">
658
+ <div class="card-title">${t('profile.access.patCreatedTitle') || 'Token created — copy it now'}</div>
659
+ <div class="access-warn-text">⚠ ${t('profile.access.patShownOnce') || 'This token is shown only once. Store it now; you cannot see it again.'}</div>
660
+ <div class="mem-item">
661
+ <div class="access-mono access-pat-token">${escHtml(created.token)}</div>
662
+ <${CopyButton} text=${created.token} className="btn-sm"
663
+ onCopied=${() => showToast(t('profile.access.patCopied') || 'Token copied')} />
664
+ </div>
665
+ <div class="mem-item access-pat-prompt-row">
666
+ <span class="mem-key">${t('profile.access.patAgentPrompt') || 'Ready-made agent prompt'}</span>
667
+ <${CopyButton} text=${created.prompt} className="btn-sm"
668
+ onCopied=${() => showToast(t('profile.access.patPromptCopied') || 'Prompt copied')} />
669
+ </div>
670
+ <div class="form-actions">
671
+ <button class="btn-ghost btn-sm" onClick=${() => setCreated(null)}>${t('profile.access.patDone') || 'Done'}</button>
672
+ </div>
673
+ </div>
674
+ `}
675
+
676
+ ${tokens === null
677
+ ? html`<div class="empty">${t('profile.access.patLoading') || 'Loading...'}</div>`
678
+ : tokens.length === 0
679
+ ? html`<div class="empty">${t('profile.access.patEmpty') || 'No access tokens yet.'}</div>`
680
+ : tokens.map(tok => html`
681
+ <div class="card" key=${tok.id}>
682
+ <div class="flex-between">
683
+ <div class="card-title">${escHtml(tok.label)}</div>
684
+ ${levelBadge(tok)}
685
+ </div>
686
+ <div class="detail-grid">
687
+ <div class="detail-item"><span class="detail-label">${t('profile.access.patCreatedAt') || 'Created'}</span><span class="detail-value">${new Date(tok.created_at).toLocaleDateString()}</span></div>
688
+ <div class="detail-item"><span class="detail-label">${t('profile.access.patLastUsed') || 'Last used'}</span><span class="detail-value">${tok.last_used_at ? new Date(tok.last_used_at).toLocaleString() : (t('profile.access.patNever') || 'never')}</span></div>
689
+ <div class="detail-item"><span class="detail-label">${t('profile.access.patExpires') || 'Expires'}</span><span class="detail-value">${tok.expires_at ? new Date(tok.expires_at).toLocaleDateString() : (t('profile.access.patNoExpiry') || 'never')}</span></div>
690
+ </div>
691
+ ${!tok.grant_owner && !tok.grant_operator && (tok.scopes || []).length > 0 && html`
692
+ <div class="flex-row-wrap mt-half">
693
+ ${tok.scopes.map(s => html`<span class="badge badge-muted" key=${s}>${s}</span>`)}
694
+ </div>
695
+ `}
696
+ <div class="card-actions">
697
+ <button class="btn-danger-solid btn-sm" onClick=${() => handleRevoke(tok.id, tok.label)}>${t('profile.access.patRevoke') || 'Revoke'}</button>
698
+ </div>
699
+ </div>
700
+ `)
701
+ }
702
+
703
+ ${!showCreate ? html`
704
+ <div class="mb-1"><button class="btn-primary" onClick=${() => setShowCreate(true)}>${t('profile.access.patCreate') || 'New Token'}</button></div>
705
+ ` : html`
706
+ <div class="create-form">
707
+ <h4 class="card-h3 mb-half">${t('profile.access.patCreateTitle') || 'Create Access Token'}</h4>
708
+ <div class="flex-col">
709
+ <div class="form-row">
710
+ <label>${t('profile.access.patLabel') || 'Name'}</label>
711
+ <input type="text" class="input-field input-sm"
712
+ placeholder=${t('profile.access.patLabelPlaceholder') || 'e.g. App tester'}
713
+ value=${label} onInput=${e => setLabel(e.target.value)} />
714
+ </div>
715
+
716
+ <div class="form-row">
717
+ <label>${t('profile.access.patLevel') || 'Permissions'}</label>
718
+ <label class="flex-row">
719
+ <input type="checkbox" checked=${grantOwner} disabled=${grantOperator}
720
+ onChange=${() => setGrantOwner(v => !v)} />
721
+ <strong>${t('profile.access.patFullOwner') || 'Full owner'}</strong> — ${t('profile.access.patFullOwnerHint') || 'acts as you; sees all apps & data'}
722
+ </label>
723
+ ${isOperator && html`
724
+ <label class="flex-row">
725
+ <input type="checkbox" checked=${grantOperator}
726
+ onChange=${() => setGrantOperator(v => !v)} />
727
+ <strong>${t('profile.access.patOperator') || 'Operator'}</strong> — ${t('profile.access.patOperatorHint') || 'full node administration (master key)'}
728
+ </label>
729
+ `}
730
+ ${fullLevel && html`<span class="badge badge-warn">⚠ ${t('profile.access.patMasterKeyWarn') || 'Master key — guard it like a password'}</span>`}
731
+ </div>
732
+
733
+ ${!fullLevel && html`
734
+ <div class="form-row">
735
+ <label>${t('profile.access.patSelectScopes') || 'Scopes'}</label>
736
+ ${PAT_SCOPE_DOMAINS.map(dom => html`
737
+ <div class="flex-row-wrap access-pat-scope-row" key=${dom.key}>
738
+ <span class="access-pat-scope-domain">${dom.key}</span>
739
+ ${dom.permissions.map(p => {
740
+ const s = `${dom.key}:${p}`;
741
+ return html`<label class="flex-row" key=${s}>
742
+ <input type="checkbox" checked=${!!selScopes[s]} onChange=${() => toggleScope(s)} /> ${p}
743
+ </label>`;
744
+ })}
745
+ </div>
746
+ `)}
747
+ <label class="flex-row mt-half">
748
+ <input type="checkbox" checked=${readOwnerData} onChange=${() => setReadOwnerData(v => !v)} />
749
+ ${t('profile.access.patReadOwnerData') || 'Also read my data (otherwise a clean sandbox)'}
750
+ </label>
751
+ </div>
752
+ `}
753
+
754
+ <div class="form-row">
755
+ <label>${t('profile.access.patExpiry') || 'Expiry'}</label>
756
+ <select class="input-field input-sm" value=${expiry} onChange=${e => setExpiry(e.target.value)}>
757
+ <option value="">${t('profile.access.patNoExpiry') || 'No expiry'}</option>
758
+ <option value="86400">${t('profile.access.patExpiry1d') || '24 hours'}</option>
759
+ <option value="604800">${t('profile.access.patExpiry7d') || '7 days'}</option>
760
+ <option value="2592000">${t('profile.access.patExpiry30d') || '30 days'}</option>
761
+ </select>
762
+ </div>
763
+
764
+ <div class="form-actions">
765
+ <button class="btn-primary btn-sm" onClick=${handleCreate} disabled=${creating}>
766
+ ${creating ? '...' : (t('profile.access.patCreateBtn') || 'Create Token')}
767
+ </button>
768
+ <button class="btn-ghost btn-sm" onClick=${resetForm}>${t('profile.access.patCancel') || 'Cancel'}</button>
769
+ </div>
770
+ </div>
771
+ </div>
772
+ `}
773
+ <${ConfirmUI} />
774
+ `;
775
+ }
776
+
535
777
  /* ═══════════════════════════════════════════════════════════════════
536
778
  Main Access Tab
537
779
  ═══════════════════════════════════════════════════════════════════ */
@@ -627,7 +869,13 @@ export default function AccessTab({ session, showToast }) {
627
869
  onMouseEnter=${() => setKeyBlurred(false)} onMouseLeave=${() => setKeyBlurred(true)}>
628
870
  ${escHtml(ownerKey)}
629
871
  </div>
630
- <span class="badge badge-warn">${t('profile.access.hoverReveal')}</span>
872
+ <div class="flex-row">
873
+ <span class="badge badge-warn">${t('profile.access.hoverReveal')}</span>
874
+ <span onClick=${(e) => e.stopPropagation()}>
875
+ <${CopyButton} text=${ownerKey} className="btn-sm"
876
+ onCopied=${() => showToast(t('profile.access.keyCopied'))} />
877
+ </span>
878
+ </div>
631
879
  </div>
632
880
  <div class="access-warn-text">⚠ ${t('profile.access.keepSafe')}</div>
633
881
  </div>
@@ -678,6 +926,7 @@ export default function AccessTab({ session, showToast }) {
678
926
  </div>
679
927
  </div>
680
928
 
929
+ <${AccessTokensSection} session=${session} showToast=${showToast} />
681
930
  <${SharingGroupsSection} showToast=${showToast} />
682
931
  <${AgentDefaultsSection} showToast=${showToast} />
683
932
  `;
@@ -3,6 +3,20 @@
3
3
  * @description Agent card component with collapsed/expanded states,
4
4
  * Two-Zone Header (identity + state-dependent status), and tab bar.
5
5
  * @version-history
6
+ * v1.12.0 -- 2026-06-02 -- Active tracked tab never shows its own badge and is
7
+ * kept marked-seen as new items arrive (so a message landing while you're on
8
+ * the Messages tab doesn't raise a (1) you're already looking at).
9
+ * v1.11.0 -- 2026-06-02 -- Unseen-change badges: collapsed-card mini-badge
10
+ * (total new across Tasks/Messages/Memory) + per-tab number badges on the
11
+ * Tasks/Messages/Memory tab buttons; opening a tracked tab clears its badge
12
+ * (via onTabSeen). Counts come from the `changes` prop (localStorage
13
+ * last-seen baseline computed in agents-tab.js).
14
+ * v1.10.0 -- 2026-06-01 -- Reorder TABS to owner-requested order: Integration,
15
+ * Tasks, Messages, Data Access, Quality, Activity, then Directives /
16
+ * Agent Config / Services (README stays prepended/first).
17
+ * v1.9.0 -- 2026-06-01 -- Reorder TABS by owner usage frequency (Integration,
18
+ * Tasks, Data Access, Quality, Activity, ... setup tabs last) so the
19
+ * now-wrapping tab bar keeps the most-used tabs on the top row.
6
20
  * v1.8.0 -- 2026-05-31 -- Add Quality tab (per-context reviews + performance + custom stats)
7
21
  * v1.0.0 -- 2026-05-24 -- Initial creation for Agent Detail Tab-View
8
22
  * v1.1.0 -- 2026-05-24 -- Fix C6: remove GAII from Zone 1, M1: add Next prefix, C1: production stats, C2: problem action buttons
@@ -37,6 +51,7 @@ import TabDataAccess from './tab-data-access.js';
37
51
  import TabDirectives from './tab-directives.js';
38
52
  import TabAgentConfig from './tab-agent-config.js';
39
53
  import TabActivity from './tab-activity.js';
54
+ import TabSchedules from './tab-schedules.js';
40
55
  import TabQuality from './tab-quality.js';
41
56
  import TabServices from './tab-services.js';
42
57
 
@@ -44,15 +59,23 @@ const html = htm.bind(h);
44
59
 
45
60
  // README is opt-in and prepended only when the agent has published one, so it
46
61
  // is NOT in this base list. See readmeTab below.
62
+ //
63
+ // Explicit owner-requested order (not auto-grouping). The tab bar wraps to a
64
+ // second row (flex-wrap, see .pf-agd-tabs CSS) instead of scrolling, so the
65
+ // first-listed tabs naturally land on the top row. README is prepended
66
+ // separately when present, so the effective order is:
67
+ // README · Integration · Tasks · Messages · Data Access · Quality · Activity
68
+ // · Directives · Agent Config · Services
47
69
  const TABS = [
48
70
  { id: 'integration', key: 'profile.agents.detail.tabs.integration' },
49
71
  { id: 'tasks', key: 'profile.agents.detail.tabs.tasks' },
50
72
  { id: 'messages', key: 'profile.agents.detail.tabs.messages' },
51
73
  { id: 'data-access', key: 'profile.agents.detail.tabs.data_access' },
74
+ { id: 'quality', key: 'profile.agents.detail.tabs.quality' },
75
+ { id: 'activity', key: 'profile.agents.detail.tabs.activity' },
76
+ { id: 'schedules', key: 'profile.agents.detail.tabs.schedules' },
52
77
  { id: 'directives', key: 'profile.agents.detail.tabs.directives' },
53
78
  { id: 'agent-config', key: 'profile.agents.detail.tabs.agent_config' },
54
- { id: 'activity', key: 'profile.agents.detail.tabs.activity' },
55
- { id: 'quality', key: 'profile.agents.detail.tabs.quality' },
56
79
  { id: 'services', key: 'profile.agents.detail.tabs.services' },
57
80
  ];
58
81
 
@@ -78,7 +101,17 @@ async function fetchReadme(agent) {
78
101
  }
79
102
  }
80
103
 
81
- export default function AgentCard({ agent, onboarding, expanded, onToggle, session, showToast, allAgents, onScopesClick, onDeleteClick, onFederateToggle, onPopOut, soloMode = false }) {
104
+ // Maps a tab id to its change-count key in the `changes` prop. Only these three
105
+ // tabs carry unseen-change badges (Tasks, Messages, Memory/data-access).
106
+ const TAB_CHANGE_KEY = { tasks: 'tasks', messages: 'messages', 'data-access': 'memory' };
107
+
108
+ // Total unseen changes across the three tracked tabs (collapsed mini-badge).
109
+ function totalChanges(changes) {
110
+ if (!changes) return 0;
111
+ return (changes.tasks || 0) + (changes.messages || 0) + (changes.memory || 0);
112
+ }
113
+
114
+ export default function AgentCard({ agent, onboarding, expanded, onToggle, session, showToast, allAgents, changes, onTabSeen, onScopesClick, onDeleteClick, onFederateToggle, onPopOut, soloMode = false }) {
82
115
  const state = detectAgentState(agent, onboarding);
83
116
  const [activeTab, setActiveTab] = useState(null);
84
117
  // README markdown: undefined = not loaded, '' = none published, string = show tab.
@@ -108,6 +141,16 @@ export default function AgentCard({ agent, onboarding, expanded, onToggle, sessi
108
141
  setActiveTab(hasReadme ? 'readme' : getDefaultTab(state));
109
142
  }, [expanded, readme]);
110
143
 
144
+ // While the owner is actively viewing a tracked tab (Tasks/Messages/Memory),
145
+ // keep it marked-seen as new items arrive. Without this, a message landing
146
+ // while you're on the Messages tab would raise its (1) badge even though you
147
+ // are looking right at it. Non-active tabs still accumulate their badge.
148
+ useEffect(() => {
149
+ if (!expanded) return;
150
+ const key = TAB_CHANGE_KEY[activeTab];
151
+ if (key && onTabSeen && changes?.[key]) onTabSeen(agent.name, key);
152
+ }, [expanded, activeTab, changes, agent.name]);
153
+
111
154
  const handleCollapse = useCallback(() => {
112
155
  // In solo mode the card is the whole window — clicking the header must not
113
156
  // collapse it (there is nothing to collapse back into).
@@ -125,6 +168,7 @@ export default function AgentCard({ agent, onboarding, expanded, onToggle, sessi
125
168
  <span class="pf-agd-expand-icon">▶</span>
126
169
  <span class="pf-agd-collapsed-icon">🤖</span>
127
170
  <span class="pf-agd-collapsed-name">${agent.display_name || agent.name}</span>
171
+ ${renderChangeBadge(changes)}
128
172
  <div class="pf-agd-collapsed-badges">
129
173
  ${renderModeBadge(agent)}
130
174
  ${renderPlatformBadge(onboarding)}
@@ -189,11 +233,22 @@ export default function AgentCard({ agent, onboarding, expanded, onToggle, sessi
189
233
  <div class="pf-agd-tabs">
190
234
  ${tabs.map(tab => {
191
235
  const label = t(tab.key);
236
+ const changeKey = TAB_CHANGE_KEY[tab.id];
237
+ // The tab you're currently viewing never shows a badge — you're
238
+ // looking at it (and the effect above keeps it marked-seen).
239
+ const count = (changeKey && activeTab !== tab.id) ? (changes?.[changeKey] || 0) : 0;
192
240
  return html`
193
241
  <button key=${tab.id}
194
242
  class="pf-agd-tab ${activeTab === tab.id ? 'pf-agd-tab--active' : ''}"
195
- onClick=${(e) => { e.stopPropagation(); userPickedTab.current = true; setActiveTab(tab.id); }}>
243
+ onClick=${(e) => {
244
+ e.stopPropagation();
245
+ userPickedTab.current = true;
246
+ setActiveTab(tab.id);
247
+ // Opening a tracked tab clears its unseen badge.
248
+ if (changeKey && onTabSeen) onTabSeen(agent.name, changeKey);
249
+ }}>
196
250
  ${label !== tab.key ? label : tab.id.charAt(0).toUpperCase() + tab.id.slice(1)}
251
+ ${count > 0 ? html`<span class="pf-agd-tab-badge">${count > 99 ? '99+' : count}</span>` : ''}
197
252
  </button>
198
253
  `;
199
254
  })}
@@ -239,6 +294,21 @@ function renderPopOut(onPopOut, agent) {
239
294
  onClick=${(e) => { e.stopPropagation(); onPopOut(agent); }}>⤢</button>`;
240
295
  }
241
296
 
297
+ // Collapsed-card mini-badge: total unseen changes across Tasks/Messages/Memory,
298
+ // so the owner can spot at a glance which agent has something new. Hidden when
299
+ // nothing changed; naturally absent from the expanded view (which does not
300
+ // render the collapsed bar). The title spells out the per-tab breakdown.
301
+ function renderChangeBadge(changes) {
302
+ const total = totalChanges(changes);
303
+ if (total <= 0) return null;
304
+ const parts = [];
305
+ if (changes.tasks) parts.push(`${t('profile.agents.detail.tabs.tasks')}: ${changes.tasks}`);
306
+ if (changes.messages) parts.push(`${t('profile.agents.detail.tabs.messages')}: ${changes.messages}`);
307
+ if (changes.memory) parts.push(`${t('profile.agents.detail.tabs.data_access')}: ${changes.memory}`);
308
+ const title = `${t('profile.agents.detail.changes.title')} — ${parts.join(', ')}`;
309
+ return html`<span class="pf-agd-change-badge" title=${title}>${total > 99 ? '99+' : total}</span>`;
310
+ }
311
+
242
312
  function renderDeliveryIndicator(agent) {
243
313
  const hasWebhook = agent.webhookUrl || agent.webhook_url;
244
314
  const hasMcp = agent.mcpEnabled || agent.mcp_enabled;
@@ -491,6 +561,7 @@ function renderTabContent(activeTab, agent, onboarding, session, showToast, allA
491
561
  case 'directives': return html`<${TabDirectives} ...${props} />`;
492
562
  case 'agent-config': return html`<${TabAgentConfig} ...${props} />`;
493
563
  case 'activity': return html`<${TabActivity} ...${props} />`;
564
+ case 'schedules': return html`<${TabSchedules} ...${props} />`;
494
565
  case 'quality': return html`<${TabQuality} ...${props} />`;
495
566
  case 'services': return html`<${TabServices} ...${props} />`;
496
567
  default: return null;
@@ -53,8 +53,8 @@ export default function TabActivity({ agent, agentName, session, showToast }) {
53
53
  const [logPage, setLogPage] = useState(1);
54
54
  const [hasMore, setHasMore] = useState(false);
55
55
 
56
- async function loadData() {
57
- setLoading(true);
56
+ async function loadData({ showSpinner = true } = {}) {
57
+ if (showSpinner) setLoading(true);
58
58
  try {
59
59
  const [actResp, logResp, dirResp, whResp, telResp] = await Promise.all([
60
60
  getActivity(agentName, 30).catch(() => null),
@@ -113,7 +113,9 @@ export default function TabActivity({ agent, agentName, session, showToast }) {
113
113
  const loadRef = useRef(loadData);
114
114
  loadRef.current = loadData;
115
115
  useEffect(() => {
116
- const handler = () => loadRef.current();
116
+ // Live-update refetch must NOT show the spinner -- it fires often and would
117
+ // flash the whole tab blank. First mount still shows the spinner.
118
+ const handler = () => loadRef.current({ showSpinner: false });
117
119
  window.addEventListener('aimeat-live-update', handler);
118
120
  return () => window.removeEventListener('aimeat-live-update', handler);
119
121
  }, []);
@@ -139,18 +141,18 @@ export default function TabActivity({ agent, agentName, session, showToast }) {
139
141
  <div>
140
142
  <!-- Stats summary -->
141
143
  ${stats && html`
142
- <div class="pf-agd-stats-grid">
143
- <div class="pf-agd-stat-card">
144
- <div class="pf-agd-stat-value">${stats.tasksCompleted ?? 0}</div>
145
- <div class="pf-agd-stat-label">${t('profile.agents.activity.tasksCompleted')}</div>
144
+ <div class="stat-grid">
145
+ <div class="stat-card">
146
+ <div class="stat-card-value">${stats.tasksCompleted ?? 0}</div>
147
+ <div class="stat-card-label">${t('profile.agents.activity.tasksCompleted')}</div>
146
148
  </div>
147
- <div class="pf-agd-stat-card">
148
- <div class="pf-agd-stat-value">${stats.tokensUsed30d ?? 0}</div>
149
- <div class="pf-agd-stat-label">${t('profile.agents.activity.tokensUsed')}</div>
149
+ <div class="stat-card">
150
+ <div class="stat-card-value">${stats.tokensUsed30d ?? 0}</div>
151
+ <div class="stat-card-label">${t('profile.agents.activity.tokensUsed')}</div>
150
152
  </div>
151
- <div class="pf-agd-stat-card">
152
- <div class="pf-agd-stat-value">${stats.successRate != null ? `${Math.round(stats.successRate)}%` : '-'}</div>
153
- <div class="pf-agd-stat-label">${t('profile.agents.activity.successRate')}</div>
153
+ <div class="stat-card">
154
+ <div class="stat-card-value">${stats.successRate != null ? `${Math.round(stats.successRate)}%` : '-'}</div>
155
+ <div class="stat-card-label">${t('profile.agents.activity.successRate')}</div>
154
156
  </div>
155
157
  </div>
156
158
  `}