auramaxx 0.0.17 → 0.0.19

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 (296) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-build-manifest.json +70 -70
  3. package/.next/app-path-routes-manifest.json +2 -2
  4. package/.next/build-manifest.json +5 -5
  5. package/.next/prerender-manifest.json +30 -30
  6. package/.next/react-loadable-manifest.json +16 -16
  7. package/.next/server/app/_not-found/page.js.nft.json +1 -1
  8. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  9. package/.next/server/app/_not-found.html +1 -1
  10. package/.next/server/app/_not-found.rsc +5 -5
  11. package/.next/server/app/api/[...doc]/page.js.nft.json +1 -1
  12. package/.next/server/app/api/[...doc]/page_client-reference-manifest.js +1 -1
  13. package/.next/server/app/api/agent-requests/route_client-reference-manifest.js +1 -1
  14. package/.next/server/app/api/apps/install/route.js.nft.json +1 -1
  15. package/.next/server/app/api/apps/install/route_client-reference-manifest.js +1 -1
  16. package/.next/server/app/api/apps/manifests/route_client-reference-manifest.js +1 -1
  17. package/.next/server/app/api/apps/static/[...path]/route_client-reference-manifest.js +1 -1
  18. package/.next/server/app/api/docs/plain/route.js.nft.json +1 -1
  19. package/.next/server/app/api/docs/plain/route_client-reference-manifest.js +1 -1
  20. package/.next/server/app/api/events/route_client-reference-manifest.js +1 -1
  21. package/.next/server/app/api/import-from-openclaw/[channel]/route.js.nft.json +1 -1
  22. package/.next/server/app/api/import-from-openclaw/[channel]/route_client-reference-manifest.js +1 -1
  23. package/.next/server/app/api/import-from-openclaw/route.js.nft.json +1 -1
  24. package/.next/server/app/api/import-from-openclaw/route_client-reference-manifest.js +1 -1
  25. package/.next/server/app/api/import-from-openclaw/validate/[channel]/route.js.nft.json +1 -1
  26. package/.next/server/app/api/import-from-openclaw/validate/[channel]/route_client-reference-manifest.js +1 -1
  27. package/.next/server/app/api/page.js.nft.json +1 -1
  28. package/.next/server/app/api/page_client-reference-manifest.js +1 -1
  29. package/.next/server/app/api/restart/route_client-reference-manifest.js +1 -1
  30. package/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
  31. package/.next/server/app/api/version/route_client-reference-manifest.js +1 -1
  32. package/.next/server/app/api/workspace/[id]/apps/[wid]/route_client-reference-manifest.js +1 -1
  33. package/.next/server/app/api/workspace/[id]/apps/route_client-reference-manifest.js +1 -1
  34. package/.next/server/app/api/workspace/[id]/export/route_client-reference-manifest.js +1 -1
  35. package/.next/server/app/api/workspace/[id]/route_client-reference-manifest.js +1 -1
  36. package/.next/server/app/api/workspace/config/route_client-reference-manifest.js +1 -1
  37. package/.next/server/app/api/workspace/import/route_client-reference-manifest.js +1 -1
  38. package/.next/server/app/api/workspace/route_client-reference-manifest.js +1 -1
  39. package/.next/server/app/app-legacy-do-not-use/page.js +3 -3
  40. package/.next/server/app/app-legacy-do-not-use/page.js.nft.json +1 -1
  41. package/.next/server/app/app-legacy-do-not-use/page_client-reference-manifest.js +1 -1
  42. package/.next/server/app/app-legacy-do-not-use.html +1 -1
  43. package/.next/server/app/app-legacy-do-not-use.rsc +6 -6
  44. package/.next/server/app/approve/[actionId]/page.js.nft.json +1 -1
  45. package/.next/server/app/approve/[actionId]/page_client-reference-manifest.js +1 -1
  46. package/.next/server/app/docs/[...doc]/page.js.nft.json +1 -1
  47. package/.next/server/app/docs/[...doc]/page_client-reference-manifest.js +1 -1
  48. package/.next/server/app/docs/page.js.nft.json +1 -1
  49. package/.next/server/app/docs/page_client-reference-manifest.js +1 -1
  50. package/.next/server/app/health/page.js.nft.json +1 -1
  51. package/.next/server/app/health/page_client-reference-manifest.js +1 -1
  52. package/.next/server/app/health.html +1 -1
  53. package/.next/server/app/health.rsc +5 -5
  54. package/.next/server/app/hello/page.js.nft.json +1 -1
  55. package/.next/server/app/hello/page_client-reference-manifest.js +1 -1
  56. package/.next/server/app/hello.html +1 -1
  57. package/.next/server/app/hello.rsc +5 -5
  58. package/.next/server/app/index.html +1 -1
  59. package/.next/server/app/index.rsc +6 -6
  60. package/.next/server/app/page.js +9 -3
  61. package/.next/server/app/page.js.nft.json +1 -1
  62. package/.next/server/app/page_client-reference-manifest.js +1 -1
  63. package/.next/server/app/privacy/page.js.nft.json +1 -1
  64. package/.next/server/app/privacy/page_client-reference-manifest.js +1 -1
  65. package/.next/server/app/privacy.html +1 -1
  66. package/.next/server/app/privacy.rsc +5 -5
  67. package/.next/server/app/share/[token]/page.js.nft.json +1 -1
  68. package/.next/server/app/share/[token]/page_client-reference-manifest.js +1 -1
  69. package/.next/server/app/terms/page.js.nft.json +1 -1
  70. package/.next/server/app/terms/page_client-reference-manifest.js +1 -1
  71. package/.next/server/app/terms.html +1 -1
  72. package/.next/server/app/terms.rsc +5 -5
  73. package/.next/server/app/yo/page.js +1 -1
  74. package/.next/server/app/yo/page.js.nft.json +1 -1
  75. package/.next/server/app/yo/page_client-reference-manifest.js +1 -1
  76. package/.next/server/app/yo.html +1 -1
  77. package/.next/server/app/yo.rsc +6 -6
  78. package/.next/server/app-paths-manifest.json +2 -2
  79. package/.next/server/chunks/1196.js +1 -1
  80. package/.next/server/chunks/{6415.js → 2123.js} +2 -2
  81. package/.next/server/chunks/2234.js +1 -1
  82. package/.next/server/chunks/4224.js +1 -0
  83. package/.next/server/chunks/4600.js +1 -1
  84. package/.next/server/chunks/5678.js +1 -1
  85. package/.next/server/chunks/5991.js +7 -0
  86. package/.next/server/chunks/6470.js +1 -1
  87. package/.next/server/chunks/7585.js +1 -1
  88. package/.next/server/chunks/7935.js +19 -11
  89. package/.next/server/chunks/8468.js +1 -1
  90. package/.next/server/chunks/{1350.js → 9323.js} +3 -3
  91. package/.next/server/chunks/9971.js +1 -1
  92. package/.next/server/functions-config-manifest.json +1 -1
  93. package/.next/server/middleware-build-manifest.js +1 -1
  94. package/.next/server/middleware-react-loadable-manifest.js +1 -1
  95. package/.next/server/pages/404.html +1 -1
  96. package/.next/server/pages/500.html +1 -1
  97. package/.next/server/server-reference-manifest.json +1 -1
  98. package/.next/static/chunks/{1168.aaac1edbb597fe5a.js → 1168.bb1c93965872e980.js} +1 -1
  99. package/.next/static/chunks/2891-bf010357841c9ce9.js +1 -0
  100. package/.next/static/chunks/{2927.7e00cc878d9a3f52.js → 2927.3976471e61df966e.js} +1 -1
  101. package/.next/static/chunks/{3573-1b41d7b8a000d015.js → 3573-667bc4c8f56afcff.js} +1 -1
  102. package/.next/static/chunks/{3a91511d-ba215c0b5dc21ba9.js → 3a91511d-da2c74cad6c6fc33.js} +2 -2
  103. package/.next/static/chunks/4114-a375178fd94c4d8a.js +1 -0
  104. package/.next/static/chunks/{4256.48407d9abad5ea33.js → 4256.aa831e4d8c3f65bc.js} +1 -1
  105. package/.next/static/chunks/{4919-7e7cdd5efc9f2110.js → 4919-0e698cd95504e355.js} +1 -1
  106. package/.next/static/chunks/{5336-233ec7ab3807267c.js → 5336-b775f449d7433604.js} +1 -1
  107. package/.next/static/chunks/{7338-3ce17a93614f1d77.js → 5362-2e8fcdd0bc6273ca.js} +4 -4
  108. package/.next/static/chunks/67-cb9bbb430499e803.js +1 -0
  109. package/.next/static/chunks/{8273-8e92d34180669ca9.js → 8273-3b9d882c5dd6a9a8.js} +1 -1
  110. package/.next/static/chunks/8291-8f6fb6554e692552.js +1 -0
  111. package/.next/static/chunks/8370-6537b6b27df58b23.js +1 -0
  112. package/.next/static/chunks/app/app-legacy-do-not-use/page-efa348da049c5873.js +1 -0
  113. package/.next/static/chunks/app/{error-63cbe72522239cff.js → error-04a18fd49a89ee17.js} +1 -1
  114. package/.next/static/chunks/app/layout-7093dda0d106f34d.js +1 -0
  115. package/.next/static/chunks/app/page-0ec7cf48550f71e6.js +1 -0
  116. package/.next/static/chunks/app/yo/{page-cf5d6540ea31c31c.js → page-48930746b135037e.js} +1 -1
  117. package/.next/static/chunks/{webpack-79ad58260e9b10b4.js → webpack-15ec0114c9d658c2.js} +1 -1
  118. package/.next/static/css/35991998d1b5eff0.css +3 -0
  119. package/.next/trace +28 -28
  120. package/README.md +2 -2
  121. package/bin/auramaxx.js +87 -3
  122. package/package.json +8 -4
  123. package/prisma/migrations/20260301225214_social_tables/migration.sql +81 -0
  124. package/prisma/migrations/20260302120000_add_notification_hash/migration.sql +8 -0
  125. package/prisma/migrations/20260302160000_add_public_key_hex/migration.sql +5 -0
  126. package/prisma/migrations/20260302170000_add_inbound_author_public_key/migration.sql +5 -0
  127. package/prisma/schema.prisma +272 -177
  128. package/shared/credential-field-schema.ts +10 -0
  129. package/skills/auramaxx/SKILL.md +33 -4
  130. package/skills/auramaxx/docs/AGENT_SETUP.md +7 -2
  131. package/src/app/UnlockPageClient.tsx +448 -43
  132. package/src/app/app-legacy-do-not-use/page.tsx +5 -3
  133. package/src/app/globals.css +1 -1
  134. package/src/components/AgentPicker.tsx +278 -0
  135. package/src/components/CreateAgentModal.tsx +192 -0
  136. package/src/components/NotificationDrawer.tsx +15 -13
  137. package/src/components/agent/AgentSidebar.tsx +2 -19
  138. package/src/components/agent/CredentialAgent.tsx +70 -37
  139. package/src/components/agent/CredentialForm.tsx +32 -7
  140. package/src/components/agent/CredentialList.tsx +149 -72
  141. package/src/components/agents/AgentsView.tsx +1035 -0
  142. package/src/components/design-system/TextAreaInput.tsx +78 -0
  143. package/src/components/design-system/index.ts +1 -0
  144. package/src/components/hub/HubView.tsx +58 -0
  145. package/src/components/layout/LeftRail.tsx +333 -40
  146. package/src/components/layout/ViewShell.tsx +125 -0
  147. package/src/components/layout/index.ts +1 -0
  148. package/src/components/social/PrimaryHubRegistrationGate.tsx +260 -0
  149. package/src/components/social/SocialView.tsx +904 -0
  150. package/src/hooks/useSocialNotifications.ts +123 -0
  151. package/src/lib/events.ts +10 -1
  152. package/src/lib/social-client.ts +319 -0
  153. package/src/lib/view-registry.ts +22 -4
  154. package/src/server/cli/commands/agent.ts +33 -7
  155. package/src/server/cli/commands/create.ts +209 -0
  156. package/src/server/cli/commands/onboard.ts +39 -0
  157. package/src/server/cli/commands/play.ts +68 -0
  158. package/src/server/cli/commands/register-agent.ts +181 -0
  159. package/src/server/cli/commands/social.ts +648 -0
  160. package/src/server/cli/commands/start.ts +4 -3
  161. package/src/server/cron/index.ts +5 -9
  162. package/src/server/cron/jobs/social-inbound.ts +596 -0
  163. package/src/server/cron/jobs/social-outbound.ts +70 -0
  164. package/src/server/index.ts +14 -0
  165. package/src/server/lib/agent-profiles.ts +10 -1
  166. package/src/server/lib/cold.ts +0 -45
  167. package/src/server/lib/credential-import.ts +53 -47
  168. package/src/server/lib/defaults.ts +11 -0
  169. package/src/server/lib/feature-flags.ts +3 -0
  170. package/src/server/lib/hub-auth.ts +159 -0
  171. package/src/server/lib/migrate-agent-profiles.ts +96 -0
  172. package/src/server/lib/notifications.ts +12 -1
  173. package/src/server/lib/permissions.ts +10 -0
  174. package/src/server/lib/project-scope.ts +33 -5
  175. package/src/server/lib/social/create.ts +176 -0
  176. package/src/server/lib/social/global-aura-id.ts +115 -0
  177. package/src/server/lib/social/hub-rpc-client.ts +99 -0
  178. package/src/server/lib/social/hub-session.ts +6 -0
  179. package/src/server/lib/social/public-key.ts +32 -0
  180. package/src/server/lib/social/register.ts +162 -0
  181. package/src/server/lib/social/sign.ts +95 -0
  182. package/src/server/lib/social/sync.ts +301 -0
  183. package/src/server/mcp/server.ts +142 -18
  184. package/src/server/mcp/tools.ts +552 -0
  185. package/src/server/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  186. package/src/server/routes/agent-hub.ts +276 -0
  187. package/src/server/routes/agent-profiles.ts +145 -19
  188. package/src/server/routes/credentials.ts +49 -15
  189. package/src/server/routes/heartbeat.ts +3 -7
  190. package/src/server/routes/passkey.ts +7 -1
  191. package/src/server/routes/social.ts +869 -0
  192. package/src/server/routes/unlock.ts +17 -1
  193. package/src/server/routes/verified-credentials.ts +312 -0
  194. package/src/server/test-data/template.db +0 -0
  195. package/src/server/test-data/worker-1/agent-primary.json +12 -0
  196. package/src/server/test-data/worker-1/auramaxx.db +0 -0
  197. package/src/server/test-data/worker-1/backups/auramaxx.db.20260305_011404.bak +0 -0
  198. package/src/server/test-data/worker-1/backups/credentials.20260305_011404.cred-00ntf5h3.json +24 -0
  199. package/src/server/test-data/worker-1/backups/credentials.20260305_011404.cred-dh8fa9k3.json +24 -0
  200. package/src/server/test-data/worker-1/backups/credentials.20260305_011404.cred-prfhlyow.json +25 -0
  201. package/src/server/test-data/worker-1/backups/credentials.20260305_011404.cred-w26jd6zi.json +22 -0
  202. package/src/server/test-data/worker-1/config.json +33 -0
  203. package/src/server/test-data/worker-1/test.db +0 -0
  204. package/src/server/test-data/worker-10/agent-primary.json +12 -0
  205. package/src/server/test-data/worker-10/config.json +33 -0
  206. package/src/server/test-data/worker-10/test.db +0 -0
  207. package/src/server/test-data/worker-11/agent-primary.json +12 -0
  208. package/src/server/test-data/worker-11/config.json +33 -0
  209. package/src/server/test-data/worker-11/test.db +0 -0
  210. package/src/server/test-data/worker-12/agent-primary.json +12 -0
  211. package/src/server/test-data/worker-12/config.json +33 -0
  212. package/src/server/test-data/worker-12/credentials/cred-hboagned.json +25 -0
  213. package/src/server/test-data/worker-12/credentials/cred-nt14xd9t.json +24 -0
  214. package/src/server/test-data/worker-12/credentials/cred-qn59xefc.json +24 -0
  215. package/src/server/test-data/worker-12/test.db +0 -0
  216. package/src/server/test-data/worker-13/agent-primary.json +12 -0
  217. package/src/server/test-data/worker-13/approval-context.json +56 -0
  218. package/src/server/test-data/worker-13/approval-context.key +1 -0
  219. package/src/server/test-data/worker-13/config.json +33 -0
  220. package/src/server/test-data/worker-13/credentials-recently-deleted/cred-jn9yqy2m.json +20 -0
  221. package/src/server/test-data/worker-13/test.db +0 -0
  222. package/src/server/test-data/worker-14/agent-primary.json +12 -0
  223. package/src/server/test-data/worker-14/config.json +33 -0
  224. package/src/server/test-data/worker-14/test.db +0 -0
  225. package/src/server/test-data/worker-15/agent-primary.json +12 -0
  226. package/src/server/test-data/worker-15/config.json +33 -0
  227. package/src/server/test-data/worker-15/test.db +0 -0
  228. package/src/server/test-data/worker-15/update-check-4b305e76.json +5 -0
  229. package/src/server/test-data/worker-2/agent-primary.json +12 -0
  230. package/src/server/test-data/worker-2/config.json +33 -0
  231. package/src/server/test-data/worker-2/test.db +0 -0
  232. package/src/server/test-data/worker-3/agent-primary.json +12 -0
  233. package/src/server/test-data/worker-3/config.json +33 -0
  234. package/src/server/test-data/worker-3/test.db +0 -0
  235. package/src/server/test-data/worker-4/agent-primary.json +12 -0
  236. package/src/server/test-data/worker-4/config.json +33 -0
  237. package/src/server/test-data/worker-4/credentials/cred-bm6p74u5.json +24 -0
  238. package/src/server/test-data/worker-4/credentials/cred-r8eqb07i.json +25 -0
  239. package/src/server/test-data/worker-4/credentials/cred-ziv49y9h.json +24 -0
  240. package/src/server/test-data/worker-4/test.db +0 -0
  241. package/src/server/test-data/worker-5/agent-primary.json +12 -0
  242. package/src/server/test-data/worker-5/config.json +33 -0
  243. package/src/server/test-data/worker-5/credentials/cred-15gyfeht.json +25 -0
  244. package/src/server/test-data/worker-5/credentials/cred-lp0ocnfc.json +24 -0
  245. package/src/server/test-data/worker-5/credentials/cred-zfbwfxw0.json +24 -0
  246. package/src/server/test-data/worker-5/test.db +0 -0
  247. package/src/server/test-data/worker-6/agent-primary.json +12 -0
  248. package/src/server/test-data/worker-6/config.json +33 -0
  249. package/src/server/test-data/worker-6/credentials/cred-dx8li3ue.json +24 -0
  250. package/src/server/test-data/worker-6/credentials/cred-myeskqky.json +24 -0
  251. package/src/server/test-data/worker-6/credentials/cred-tam01qdt.json +25 -0
  252. package/src/server/test-data/worker-6/credentials/cred-wdh6rslw.json +22 -0
  253. package/src/server/test-data/worker-6/credentials-archive/cred-4wg53v1y.json +25 -0
  254. package/src/server/test-data/worker-6/test.db +0 -0
  255. package/src/server/test-data/worker-7/agent-primary.json +12 -0
  256. package/src/server/test-data/worker-7/config.json +33 -0
  257. package/src/server/test-data/worker-7/credentials/cred-ji7ec57d.json +25 -0
  258. package/src/server/test-data/worker-7/credentials/cred-mmgj0ypo.json +24 -0
  259. package/src/server/test-data/worker-7/credentials/cred-nb0ic93x.json +24 -0
  260. package/src/server/test-data/worker-7/test.db +0 -0
  261. package/src/server/test-data/worker-8/agent-primary.json +12 -0
  262. package/src/server/test-data/worker-8/config.json +33 -0
  263. package/src/server/test-data/worker-8/test.db +0 -0
  264. package/src/server/test-data/worker-9/agent-primary.json +12 -0
  265. package/src/server/test-data/worker-9/test.db +0 -0
  266. package/src/server/tests/cli/bin-entrypoint.test.ts +99 -0
  267. package/src/server/tests/cli/social.test.ts +83 -0
  268. package/src/server/tests/cron/social-inbound-followers.unit.test.ts +164 -0
  269. package/src/server/tests/cron/social-inbound-staleness.test.ts +193 -0
  270. package/src/server/tests/cron/social-outbound.test.ts +252 -0
  271. package/src/server/tests/endpoints/agent.test.ts +0 -16
  272. package/src/server/tests/lib/credential-field-schema.test.ts +448 -0
  273. package/src/server/tests/lib/project-scope.test.ts +49 -0
  274. package/src/server/tests/lib/social/create.test.ts +207 -0
  275. package/src/server/tests/lib/social/sign.test.ts +163 -0
  276. package/src/server/tests/lib/social/sync.test.ts +198 -0
  277. package/src/server/tests/mcp/server.test.ts +1 -3
  278. package/src/server/tests/mcp/tools.test.ts +219 -7
  279. package/src/server/tsconfig.tsbuildinfo +1 -1
  280. package/tsconfig.json +1 -0
  281. package/.next/server/chunks/2460.js +0 -7
  282. package/.next/server/chunks/5246.js +0 -1
  283. package/.next/static/chunks/3667-1db7bd03948e60df.js +0 -1
  284. package/.next/static/chunks/5442-b5bb869e832e8967.js +0 -1
  285. package/.next/static/chunks/6872-a8f1ad5fc35a1c35.js +0 -1
  286. package/.next/static/chunks/7394-4bdb1feefad1a74a.js +0 -1
  287. package/.next/static/chunks/7587-8542eae6e5139d27.js +0 -1
  288. package/.next/static/chunks/app/app-legacy-do-not-use/page-569fed5d1a2bfe79.js +0 -1
  289. package/.next/static/chunks/app/layout-af8d9969c7aeb758.js +0 -1
  290. package/.next/static/chunks/app/page-9bd0ec3ba4aa6cea.js +0 -1
  291. package/.next/static/css/88f92e1578b1e5d9.css +0 -3
  292. package/shared/agent-profile-schema.ts +0 -81
  293. package/src/internal/security/frontend-api-authorization-audit.md +0 -206
  294. package/src/server/lib/agent-profile-records.ts +0 -72
  295. /package/.next/static/{QPzVnl7GToqgYBq14V_iC → yEruVJH_cuht5hu5_1R00}/_buildManifest.js +0 -0
  296. /package/.next/static/{QPzVnl7GToqgYBq14V_iC → yEruVJH_cuht5hu5_1R00}/_ssgManifest.js +0 -0
@@ -0,0 +1,904 @@
1
+ 'use client';
2
+
3
+ import React, { useCallback, useEffect, useMemo, useState } from 'react';
4
+ import {
5
+ Rss,
6
+ FileText,
7
+ Users,
8
+ UserPlus,
9
+ BarChart3,
10
+ Globe,
11
+ Heart,
12
+ Trash2,
13
+ Loader2,
14
+ Send,
15
+ } from 'lucide-react';
16
+ import {
17
+ type AgentProfile,
18
+ type FollowerItem,
19
+ type FollowingItem,
20
+ type GlobalFeedItem,
21
+ type InboundMessageItem,
22
+ type SocialAgentInfo,
23
+ type SocialMessageItem,
24
+ type SyncStatus,
25
+ fetchGlobalFeed,
26
+ fetchSocialAgents,
27
+ fetchSocialFeed,
28
+ fetchSocialFollowers,
29
+ fetchSocialFollowing,
30
+ fetchSocialPosts,
31
+ fetchPrimaryHubUrl,
32
+ fetchSocialProfile,
33
+ fetchSocialStatus,
34
+ likeSocialPost,
35
+ removeSocialPost,
36
+ submitPostLocalFirst,
37
+ } from '@/lib/social-client';
38
+ import { ViewShell } from '@/components/layout/ViewShell';
39
+ import { AgentPicker, AgentPfp } from '@/components/AgentPicker';
40
+ import { CreateAgentModal } from '@/components/CreateAgentModal';
41
+ import { Button, Modal, TextAreaInput } from '@/components/design-system';
42
+
43
+ // ─── Types ───────────────────────────────────────────────────────────
44
+
45
+ type SidebarTab = 'feed' | 'discover' | 'posts' | 'followers' | 'following' | 'status';
46
+
47
+ // ─── Helpers ─────────────────────────────────────────────────────────
48
+
49
+ function formatTimestamp(ts: number): string {
50
+ // timestamps are in seconds
51
+ const date = new Date(ts * 1000);
52
+ const now = Date.now();
53
+ const diff = now - date.getTime();
54
+
55
+ if (diff < 60_000) return 'just now';
56
+ if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m ago`;
57
+ if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)}h ago`;
58
+ return date.toLocaleDateString();
59
+ }
60
+
61
+ function parseBody(body: string): Record<string, unknown> {
62
+ try {
63
+ return JSON.parse(body);
64
+ } catch {
65
+ return {};
66
+ }
67
+ }
68
+
69
+ const DEFAULT_AGENT_AVATAR = 'https://auramaxx.sh/agent7.png';
70
+
71
+ // ─── Styles ──────────────────────────────────────────────────────────
72
+
73
+ const cardStyle: React.CSSProperties = {
74
+ border: '1px solid var(--color-border, #d4d4d8)',
75
+ borderRadius: 0,
76
+ padding: '14px 16px',
77
+ background: 'var(--color-surface, #ffffff)',
78
+ borderBottom: 'none',
79
+ };
80
+
81
+ const mutedText: React.CSSProperties = {
82
+ fontSize: '10px',
83
+ color: 'var(--color-text-muted, #6b7280)',
84
+ fontFamily: 'var(--font-mono, monospace)',
85
+ letterSpacing: '0.02em',
86
+ };
87
+
88
+ const accentText: React.CSSProperties = {
89
+ fontWeight: 600,
90
+ fontFamily: 'var(--font-mono, monospace)',
91
+ color: 'var(--color-text, #0a0a0a)',
92
+ };
93
+
94
+ // ─── Sidebar Tab Button ─────────────────────────────────────────────
95
+
96
+ const TABS: { key: SidebarTab; label: string; icon: React.ComponentType<{ size: number }> }[] = [
97
+ { key: 'discover', label: 'Global', icon: Globe },
98
+ { key: 'feed', label: 'For You', icon: Rss },
99
+ { key: 'posts', label: 'My Posts', icon: FileText },
100
+ { key: 'followers', label: 'Followers', icon: Users },
101
+ { key: 'following', label: 'Following', icon: UserPlus },
102
+ { key: 'status', label: 'Sync Status', icon: BarChart3 },
103
+ ];
104
+
105
+ function TabButton({
106
+ tab,
107
+ isActive,
108
+ onClick,
109
+ badge,
110
+ }: {
111
+ tab: (typeof TABS)[number];
112
+ isActive: boolean;
113
+ onClick: () => void;
114
+ badge?: number;
115
+ }) {
116
+ const Icon = tab.icon;
117
+ return (
118
+ <button
119
+ onClick={onClick}
120
+ style={{
121
+ width: '100%',
122
+ padding: '8px 12px',
123
+ borderRadius: '8px',
124
+ border: 'none',
125
+ background: isActive ? 'var(--color-accent, #6366f1)' : 'transparent',
126
+ color: isActive ? 'var(--color-accent-foreground, #ffffff)' : 'var(--color-text, #0a0a0a)',
127
+ cursor: 'pointer',
128
+ display: 'flex',
129
+ alignItems: 'center',
130
+ gap: '8px',
131
+ fontSize: '12px',
132
+ textAlign: 'left',
133
+ transition: 'all 0.1s ease',
134
+ marginBottom: '2px',
135
+ }}
136
+ >
137
+ <Icon size={14} />
138
+ <span style={{ fontWeight: 500, flex: 1 }}>{tab.label}</span>
139
+ {badge != null && badge > 0 && (
140
+ <span
141
+ style={{
142
+ minWidth: '16px',
143
+ height: '16px',
144
+ padding: '0 4px',
145
+ borderRadius: '8px',
146
+ background: isActive ? 'rgba(255,255,255,0.3)' : 'var(--color-accent, #6366f1)',
147
+ color: isActive ? '#fff' : 'var(--color-accent-foreground, #ffffff)',
148
+ fontSize: '10px',
149
+ fontWeight: 700,
150
+ display: 'flex',
151
+ alignItems: 'center',
152
+ justifyContent: 'center',
153
+ fontFamily: 'var(--font-mono, monospace)',
154
+ }}
155
+ >
156
+ {badge > 99 ? '99+' : badge}
157
+ </span>
158
+ )}
159
+ </button>
160
+ );
161
+ }
162
+
163
+ // ─── Post Card ───────────────────────────────────────────────────────
164
+
165
+ function PostCard({
166
+ authorLabel,
167
+ authorDisplayName,
168
+ authorHandle,
169
+ authorPfp,
170
+ authorPfpIsSprite,
171
+ text,
172
+ timestamp,
173
+ hash,
174
+ hubUrl,
175
+ syncStatus,
176
+ isOwn,
177
+ agentId,
178
+ onDeleted,
179
+ onLiked,
180
+ }: {
181
+ authorLabel: string;
182
+ authorDisplayName?: string;
183
+ authorHandle?: string;
184
+ authorPfp?: string;
185
+ authorPfpIsSprite?: boolean;
186
+ text: string;
187
+ timestamp: number;
188
+ hash: string;
189
+ hubUrl?: string;
190
+ syncStatus?: string;
191
+ isOwn: boolean;
192
+ agentId: string;
193
+ onDeleted?: (hash: string) => void;
194
+ onLiked?: () => void;
195
+ }) {
196
+ const [deleting, setDeleting] = useState(false);
197
+ const [liking, setLiking] = useState(false);
198
+
199
+ const handleDelete = useCallback(async () => {
200
+ if (deleting) return;
201
+ setDeleting(true);
202
+ try {
203
+ await removeSocialPost(agentId, hash, hubUrl);
204
+ onDeleted?.(hash);
205
+ } catch {
206
+ // ignore
207
+ } finally {
208
+ setDeleting(false);
209
+ }
210
+ }, [agentId, hash, hubUrl, deleting, onDeleted]);
211
+
212
+ const handleLike = useCallback(async () => {
213
+ if (liking) return;
214
+ setLiking(true);
215
+ try {
216
+ await likeSocialPost(agentId, hash, hubUrl);
217
+ onLiked?.();
218
+ } catch {
219
+ // ignore
220
+ } finally {
221
+ setLiking(false);
222
+ }
223
+ }, [agentId, hash, hubUrl, liking, onLiked]);
224
+
225
+ return (
226
+ <div style={cardStyle}>
227
+ {/* Author row */}
228
+ <div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '10px' }}>
229
+ <AgentPfp
230
+ src={authorPfp}
231
+ isSprite={authorPfpIsSprite}
232
+ fallbackLetter={authorLabel[0] === '#' ? authorLabel[1] : authorLabel[0]}
233
+ size="md"
234
+ />
235
+ <div style={{ flex: 1, minWidth: 0 }}>
236
+ <div style={{ display: 'flex', alignItems: 'baseline', gap: '6px' }}>
237
+ <span style={{ ...accentText, fontSize: '12px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
238
+ {authorDisplayName || authorHandle || authorLabel}
239
+ </span>
240
+ {authorHandle && authorDisplayName && (
241
+ <span style={{ ...mutedText, fontSize: '10px' }}>@{authorHandle}</span>
242
+ )}
243
+ {!authorDisplayName && !authorHandle && (
244
+ <span style={{ ...mutedText, fontSize: '10px' }}>{authorLabel}</span>
245
+ )}
246
+ </div>
247
+ <div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
248
+ <span style={{ ...mutedText, fontSize: '9px' }}>{formatTimestamp(timestamp)}</span>
249
+ </div>
250
+ </div>
251
+ {syncStatus && (
252
+ <span
253
+ style={{
254
+ ...mutedText,
255
+ fontSize: '9px',
256
+ padding: '2px 6px',
257
+ border: '1px solid',
258
+ borderColor: syncStatus === 'accepted' ? '#166534' : syncStatus === 'pending' ? '#92400e' : '#991b1b',
259
+ color: syncStatus === 'accepted' ? '#166534' : syncStatus === 'pending' ? '#92400e' : '#991b1b',
260
+ textTransform: 'uppercase',
261
+ }}
262
+ >
263
+ {syncStatus}
264
+ </span>
265
+ )}
266
+ </div>
267
+
268
+ {/* Body */}
269
+ <div style={{
270
+ fontSize: '13px',
271
+ color: 'var(--color-text, #0a0a0a)',
272
+ lineHeight: 1.6,
273
+ whiteSpace: 'pre-wrap',
274
+ paddingLeft: '32px',
275
+ }}>
276
+ {text}
277
+ </div>
278
+
279
+ {/* Actions */}
280
+ <div style={{ display: 'flex', alignItems: 'center', gap: '4px', marginTop: '10px', paddingLeft: '32px' }}>
281
+ {!isOwn && (
282
+ <button
283
+ onClick={handleLike}
284
+ disabled={liking}
285
+ style={{
286
+ padding: '2px 8px',
287
+ fontSize: '9px',
288
+ fontFamily: 'var(--font-mono, monospace)',
289
+ textTransform: 'uppercase',
290
+ letterSpacing: '0.05em',
291
+ borderRadius: 0,
292
+ border: '1px solid var(--color-border, #d4d4d8)',
293
+ background: 'transparent',
294
+ color: 'var(--color-text-muted, #6b7280)',
295
+ cursor: liking ? 'not-allowed' : 'pointer',
296
+ display: 'flex',
297
+ alignItems: 'center',
298
+ gap: '4px',
299
+ transition: 'border-color 0.1s',
300
+ }}
301
+ >
302
+ <Heart size={9} />
303
+ {liking ? '...' : 'Like'}
304
+ </button>
305
+ )}
306
+ {isOwn && (
307
+ <button
308
+ onClick={handleDelete}
309
+ disabled={deleting}
310
+ style={{
311
+ padding: '2px 8px',
312
+ fontSize: '9px',
313
+ fontFamily: 'var(--font-mono, monospace)',
314
+ textTransform: 'uppercase',
315
+ letterSpacing: '0.05em',
316
+ borderRadius: 0,
317
+ border: '1px solid var(--color-border, #d4d4d8)',
318
+ background: 'transparent',
319
+ color: '#ef4444',
320
+ cursor: deleting ? 'not-allowed' : 'pointer',
321
+ display: 'flex',
322
+ alignItems: 'center',
323
+ gap: '4px',
324
+ }}
325
+ >
326
+ <Trash2 size={9} />
327
+ {deleting ? '...' : 'Delete'}
328
+ </button>
329
+ )}
330
+ </div>
331
+ </div>
332
+ );
333
+ }
334
+
335
+ // ─── Main Component ─────────────────────────────────────────────────
336
+
337
+ export function SocialView({ activeHubUrl }: { activeHubUrl?: string }) {
338
+ const [activeTab, setActiveTab] = useState<SidebarTab>('feed');
339
+ const [agents, setAgents] = useState<SocialAgentInfo[]>([]);
340
+ const [agentId, setAgentId] = useState<string>('');
341
+ const [primaryHubUrl, setPrimaryHubUrl] = useState<string>('');
342
+ const [agentProfiles, setAgentProfiles] = useState<Record<string, AgentProfile>>({});
343
+ const [loading, setLoading] = useState(true);
344
+ const [error, setError] = useState<string | null>(null);
345
+ const [showCreateAgent, setShowCreateAgent] = useState(false);
346
+ const [showPostModal, setShowPostModal] = useState(false);
347
+ const [postText, setPostText] = useState('');
348
+ const [postError, setPostError] = useState<string | null>(null);
349
+ const [posting, setPosting] = useState(false);
350
+
351
+ // Data for each tab
352
+ const [feedItems, setFeedItems] = useState<InboundMessageItem[]>([]);
353
+ const [globalFeed, setGlobalFeed] = useState<GlobalFeedItem[]>([]);
354
+ const [globalLoading, setGlobalLoading] = useState(false);
355
+ const [myPosts, setMyPosts] = useState<SocialMessageItem[]>([]);
356
+ const [followers, setFollowers] = useState<FollowerItem[]>([]);
357
+ const [following, setFollowing] = useState<FollowingItem[]>([]);
358
+ const [syncStatus, setSyncStatus] = useState<SyncStatus | null>(null);
359
+ const [tabLoading, setTabLoading] = useState(false);
360
+
361
+ // Revision counter to force refresh
362
+ const [revision, setRevision] = useState(0);
363
+ const refresh = useCallback(() => setRevision((r) => r + 1), []);
364
+
365
+ // Search
366
+ const [searchValue, setSearchValue] = useState('');
367
+ const effectiveHubUrl = primaryHubUrl || activeHubUrl;
368
+
369
+ // Social always targets the primary hub from settings (`social.hub_url`).
370
+ useEffect(() => {
371
+ let cancelled = false;
372
+ (async () => {
373
+ try {
374
+ const hubUrl = await fetchPrimaryHubUrl();
375
+ if (!cancelled) setPrimaryHubUrl(hubUrl);
376
+ } catch {
377
+ // Fall back to caller-provided hub URL when defaults route is unavailable.
378
+ if (!cancelled) setPrimaryHubUrl('');
379
+ }
380
+ })();
381
+ return () => { cancelled = true; };
382
+ }, []);
383
+
384
+ const filteredFeed = useMemo(() => {
385
+ if (!searchValue.trim()) return feedItems;
386
+ const q = searchValue.toLowerCase();
387
+ return feedItems.filter((item) => {
388
+ const body = parseBody(item.body);
389
+ return (body.text as string)?.toLowerCase().includes(q) || String(item.authorAuraId).includes(q);
390
+ });
391
+ }, [feedItems, searchValue]);
392
+
393
+ const filteredPosts = useMemo(() => {
394
+ if (!searchValue.trim()) return myPosts;
395
+ const q = searchValue.toLowerCase();
396
+ return myPosts.filter((item) => {
397
+ const body = parseBody(item.body);
398
+ return (body.text as string)?.toLowerCase().includes(q);
399
+ });
400
+ }, [myPosts, searchValue]);
401
+
402
+ const filteredGlobalFeed = useMemo(() => {
403
+ if (!searchValue.trim()) return globalFeed;
404
+ const q = searchValue.toLowerCase();
405
+ return globalFeed.filter((item) =>
406
+ item.text?.toLowerCase().includes(q) || String(item.authorAuraId).includes(q),
407
+ );
408
+ }, [globalFeed, searchValue]);
409
+
410
+ const filteredFollowers = useMemo(() => {
411
+ if (!searchValue.trim()) return followers;
412
+ const q = searchValue.toLowerCase();
413
+ return followers.filter((f) => String(f.authorAuraId).includes(q));
414
+ }, [followers, searchValue]);
415
+
416
+ const filteredFollowing = useMemo(() => {
417
+ if (!searchValue.trim()) return following;
418
+ const q = searchValue.toLowerCase();
419
+ return following.filter((f) => String(f.auraId).includes(q));
420
+ }, [following, searchValue]);
421
+
422
+ // Load agents on mount
423
+ useEffect(() => {
424
+ let cancelled = false;
425
+ (async () => {
426
+ try {
427
+ const loadedAgents = await fetchSocialAgents();
428
+ if (cancelled) return;
429
+ setAgents(loadedAgents);
430
+ if (loadedAgents.length > 0) {
431
+ const defaultAgentId = loadedAgents.find((agent) => agent.isPrimary)?.id ?? loadedAgents[0].id;
432
+ setAgentId(defaultAgentId);
433
+ }
434
+ } catch {
435
+ if (!cancelled) setError('Failed to load agents');
436
+ } finally {
437
+ if (!cancelled) setLoading(false);
438
+ }
439
+ })();
440
+ return () => { cancelled = true; };
441
+ }, []);
442
+
443
+ const refreshAgents = useCallback(async () => {
444
+ try {
445
+ const loadedAgents = await fetchSocialAgents();
446
+ setAgents(loadedAgents);
447
+ if (loadedAgents.length === 0) return;
448
+ setAgentId((current) => {
449
+ if (current && loadedAgents.some((agent) => agent.id === current)) return current;
450
+ return loadedAgents.find((agent) => agent.isPrimary)?.id ?? loadedAgents[0].id;
451
+ });
452
+ } catch { /* silent */ }
453
+ }, []);
454
+
455
+ // Load tab data when agentId or tab changes
456
+ useEffect(() => {
457
+ if (!agentId) return;
458
+ let cancelled = false;
459
+ setTabLoading(true);
460
+
461
+ (async () => {
462
+ try {
463
+ switch (activeTab) {
464
+ case 'feed': {
465
+ const nextFeed = await fetchSocialFeed(agentId, effectiveHubUrl);
466
+ if (!cancelled) setFeedItems(nextFeed);
467
+ break;
468
+ }
469
+ case 'posts': {
470
+ const nextPosts = await fetchSocialPosts(agentId, effectiveHubUrl);
471
+ if (!cancelled) setMyPosts(nextPosts);
472
+ break;
473
+ }
474
+ case 'followers': {
475
+ const nextFollowers = await fetchSocialFollowers(agentId, effectiveHubUrl);
476
+ if (!cancelled) setFollowers(nextFollowers);
477
+ break;
478
+ }
479
+ case 'following': {
480
+ const nextFollowing = await fetchSocialFollowing(agentId, effectiveHubUrl);
481
+ if (!cancelled) setFollowing(nextFollowing);
482
+ break;
483
+ }
484
+ case 'status': {
485
+ const nextStatus = await fetchSocialStatus(agentId, effectiveHubUrl);
486
+ if (!cancelled) setSyncStatus(nextStatus);
487
+ break;
488
+ }
489
+ }
490
+ } catch {
491
+ // individual tab errors are non-fatal
492
+ } finally {
493
+ if (!cancelled) setTabLoading(false);
494
+ }
495
+ })();
496
+
497
+ return () => { cancelled = true; };
498
+ }, [agentId, activeTab, revision, effectiveHubUrl]);
499
+
500
+ // Load global feed when discover tab is selected (no agentId dependency)
501
+ useEffect(() => {
502
+ if (activeTab !== 'discover') return;
503
+ let cancelled = false;
504
+ setGlobalLoading(true);
505
+
506
+ (async () => {
507
+ try {
508
+ const result = await fetchGlobalFeed(undefined, effectiveHubUrl);
509
+ if (!cancelled) setGlobalFeed(result.items);
510
+ } catch {
511
+ // non-fatal
512
+ } finally {
513
+ if (!cancelled) setGlobalLoading(false);
514
+ }
515
+ })();
516
+
517
+ return () => { cancelled = true; };
518
+ }, [activeTab, revision, effectiveHubUrl]);
519
+
520
+ useEffect(() => {
521
+ if (!agentId || agentProfiles[agentId]) return;
522
+ let cancelled = false;
523
+
524
+ (async () => {
525
+ try {
526
+ const profile = await fetchSocialProfile(agentId);
527
+ if (cancelled) return;
528
+ setAgentProfiles((prev) => ({ ...prev, [agentId]: profile }));
529
+ } catch {
530
+ if (cancelled) return;
531
+ setAgentProfiles((prev) => (
532
+ prev[agentId]
533
+ ? prev
534
+ : {
535
+ ...prev,
536
+ [agentId]: {
537
+ pfp: DEFAULT_AGENT_AVATAR,
538
+ handle: '',
539
+ displayName: '',
540
+ },
541
+ }
542
+ ));
543
+ }
544
+ })();
545
+
546
+ return () => { cancelled = true; };
547
+ }, [agentId, agentProfiles]);
548
+
549
+ const handleSubmitPost = useCallback(async (text: string) => {
550
+ if (!agentId) return;
551
+
552
+ const { localMessage, submit } = submitPostLocalFirst(agentId, text, effectiveHubUrl);
553
+ setMyPosts((prev) => [localMessage, ...prev.filter((item) => item.id !== localMessage.id)]);
554
+
555
+ try {
556
+ const persisted = await submit;
557
+ setMyPosts((prev) => [
558
+ persisted,
559
+ ...prev.filter((item) => item.id !== localMessage.id && item.id !== persisted.id),
560
+ ]);
561
+ refresh();
562
+ } catch (error) {
563
+ setMyPosts((prev) => prev.map((item) => (
564
+ item.id === localMessage.id
565
+ ? { ...item, syncStatus: 'failed' }
566
+ : item
567
+ )));
568
+ throw error;
569
+ }
570
+ }, [agentId, refresh, effectiveHubUrl]);
571
+
572
+ const handleClosePostModal = useCallback(() => {
573
+ if (posting) return;
574
+ setShowPostModal(false);
575
+ setPostError(null);
576
+ }, [posting]);
577
+
578
+ const handlePostFromModal = useCallback(async () => {
579
+ const normalized = postText.trim();
580
+ if (!normalized || posting) return;
581
+ setPosting(true);
582
+ setPostError(null);
583
+ try {
584
+ await handleSubmitPost(normalized);
585
+ setPostText('');
586
+ setShowPostModal(false);
587
+ setActiveTab('posts');
588
+ } catch (err) {
589
+ setPostError(err instanceof Error ? err.message : 'Post failed');
590
+ } finally {
591
+ setPosting(false);
592
+ }
593
+ }, [handleSubmitPost, postText, posting]);
594
+
595
+ // Only parent agents (no child / linked agents) — must be before early returns
596
+ const parentAgents = useMemo(() => agents.filter(a => !a.parentAgentId), [agents]);
597
+ const primaryAgentId = useMemo(() => agents.find(a => a.isPrimary)?.id, [agents]);
598
+
599
+ if (loading) {
600
+ return (
601
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%' }}>
602
+ <Loader2 size={20} className="animate-spin" style={{ color: 'var(--color-text-muted, #6b7280)' }} />
603
+ </div>
604
+ );
605
+ }
606
+
607
+ // ─── Sidebar content ───
608
+
609
+ const sidebarContent = (
610
+ <div style={{ padding: '8px' }}>
611
+ <Button
612
+ variant="primary"
613
+ size="sm"
614
+ icon={<Send size={12} />}
615
+ disabled={!agentId}
616
+ onClick={() => {
617
+ setPostError(null);
618
+ setShowPostModal(true);
619
+ }}
620
+ className="w-full mb-2"
621
+ >
622
+ NEW POST
623
+ </Button>
624
+ {/* Tabs */}
625
+ {TABS.map((tab) => (
626
+ <TabButton
627
+ key={tab.key}
628
+ tab={tab}
629
+ isActive={tab.key === activeTab}
630
+ onClick={() => setActiveTab(tab.key)}
631
+ badge={undefined}
632
+ />
633
+ ))}
634
+ </div>
635
+ );
636
+
637
+ const sidebarFooter = (
638
+ <div style={{ background: 'var(--color-background-alt, #f4f4f5)' }}>
639
+ <AgentPicker
640
+ agents={parentAgents}
641
+ profiles={agentProfiles}
642
+ selectedId={agentId || null}
643
+ onSelect={setAgentId}
644
+ onCreateAgent={() => setShowCreateAgent(true)}
645
+ direction="up"
646
+ />
647
+ </div>
648
+ );
649
+
650
+ // ─── Main content ───
651
+
652
+ const ownProfile = agentId ? agentProfiles[agentId] : undefined;
653
+
654
+ const mainContent = (
655
+ <div style={{ padding: '24px 32px' }}>
656
+ <div style={{ maxWidth: '600px', border: '1px solid var(--color-border, #d4d4d8)', borderBottom: 'none' }}>
657
+ {error && (
658
+ <div style={{ ...cardStyle, color: '#ef4444', fontSize: '12px', borderBottom: '1px solid var(--color-border, #d4d4d8)' }}>{error}</div>
659
+ )}
660
+
661
+ {(tabLoading || globalLoading) ? (
662
+ <div style={{ display: 'flex', justifyContent: 'center', padding: '32px', borderBottom: '1px solid var(--color-border, #d4d4d8)' }}>
663
+ <Loader2 size={16} className="animate-spin" style={{ color: 'var(--color-text-muted, #6b7280)' }} />
664
+ </div>
665
+ ) : (
666
+ <>
667
+ {/* Discover tab */}
668
+ {activeTab === 'discover' && (
669
+ filteredGlobalFeed.length === 0 ? (
670
+ <div style={{ ...mutedText, textAlign: 'center', padding: '32px', borderBottom: '1px solid var(--color-border, #d4d4d8)' }}>
671
+ No posts to discover yet.
672
+ </div>
673
+ ) : (
674
+ filteredGlobalFeed.map((item) => {
675
+ const p = item.authorProfile;
676
+ const pfp = p?.pfp || DEFAULT_AGENT_AVATAR;
677
+ return (
678
+ <PostCard
679
+ key={item.postHash}
680
+ authorLabel={`#${item.authorAuraId}`}
681
+ authorDisplayName={p?.displayName || undefined}
682
+ authorHandle={p?.handle || undefined}
683
+ authorPfp={pfp}
684
+ authorPfpIsSprite={p?.pfpIsSprite ?? /\/agent\d+\.png$/.test(pfp)}
685
+ text={item.text || ''}
686
+ timestamp={item.updatedTs}
687
+ hash={item.postHash}
688
+ hubUrl={effectiveHubUrl}
689
+ isOwn={false}
690
+ agentId={agentId}
691
+ onLiked={agentId ? refresh : undefined}
692
+ />
693
+ );
694
+ })
695
+ )
696
+ )}
697
+
698
+ {/* Feed tab */}
699
+ {activeTab === 'feed' && (
700
+ filteredFeed.length === 0 ? (
701
+ <div style={{ ...mutedText, textAlign: 'center', padding: '32px', borderBottom: '1px solid var(--color-border, #d4d4d8)' }}>
702
+ No posts in your feed yet. Follow some agents to see their posts here.
703
+ </div>
704
+ ) : (
705
+ filteredFeed.map((item) => {
706
+ const body = parseBody(item.body);
707
+ const isOwn = item.authorAuraId <= 0;
708
+ return (
709
+ <PostCard
710
+ key={item.id}
711
+ authorLabel={isOwn ? 'You' : `#${item.authorAuraId}`}
712
+ authorDisplayName={isOwn ? ownProfile?.displayName : undefined}
713
+ authorHandle={isOwn ? ownProfile?.handle : undefined}
714
+ authorPfp={isOwn ? (ownProfile?.pfp || DEFAULT_AGENT_AVATAR) : undefined}
715
+ authorPfpIsSprite={isOwn ? (ownProfile?.pfpIsSprite ?? /\/agent\d+\.png$/.test(ownProfile?.pfp || DEFAULT_AGENT_AVATAR)) : undefined}
716
+ text={(body.text as string) || ''}
717
+ timestamp={item.timestamp}
718
+ hash={item.hash}
719
+ hubUrl={effectiveHubUrl}
720
+ isOwn={isOwn}
721
+ agentId={agentId}
722
+ onLiked={refresh}
723
+ />
724
+ );
725
+ })
726
+ )
727
+ )}
728
+
729
+ {/* My Posts tab */}
730
+ {activeTab === 'posts' && (
731
+ filteredPosts.length === 0 ? (
732
+ <div style={{ ...mutedText, textAlign: 'center', padding: '32px', borderBottom: '1px solid var(--color-border, #d4d4d8)' }}>
733
+ No posts yet. Write your first post above.
734
+ </div>
735
+ ) : (
736
+ filteredPosts.map((item) => {
737
+ const body = parseBody(item.body);
738
+ return (
739
+ <PostCard
740
+ key={item.id}
741
+ authorLabel="You"
742
+ authorDisplayName={ownProfile?.displayName}
743
+ authorHandle={ownProfile?.handle}
744
+ authorPfp={ownProfile?.pfp || DEFAULT_AGENT_AVATAR}
745
+ authorPfpIsSprite={ownProfile?.pfpIsSprite ?? /\/agent\d+\.png$/.test(ownProfile?.pfp || DEFAULT_AGENT_AVATAR)}
746
+ text={(body.text as string) || ''}
747
+ timestamp={item.timestamp}
748
+ hash={item.hash}
749
+ hubUrl={effectiveHubUrl}
750
+ syncStatus={item.syncStatus}
751
+ isOwn={true}
752
+ agentId={agentId}
753
+ onDeleted={(removedHash) => {
754
+ setMyPosts((prev) => prev.filter((post) => post.hash !== removedHash));
755
+ refresh();
756
+ }}
757
+ />
758
+ );
759
+ })
760
+ )
761
+ )}
762
+
763
+ {/* Followers tab */}
764
+ {activeTab === 'followers' && (
765
+ filteredFollowers.length === 0 ? (
766
+ <div style={{ ...mutedText, textAlign: 'center', padding: '32px', borderBottom: '1px solid var(--color-border, #d4d4d8)' }}>
767
+ No followers yet.
768
+ </div>
769
+ ) : (
770
+ <>
771
+ <div style={{ ...mutedText, padding: '8px 16px', borderBottom: '1px solid var(--color-border, #d4d4d8)' }}>{filteredFollowers.length} follower{filteredFollowers.length !== 1 ? 's' : ''}</div>
772
+ {filteredFollowers.map((f) => (
773
+ <div key={f.id} style={{ ...cardStyle, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
774
+ <span style={{ ...accentText, fontSize: '12px' }}>#{f.authorAuraId}</span>
775
+ <span style={mutedText}>{formatTimestamp(f.timestamp)}</span>
776
+ </div>
777
+ ))}
778
+ </>
779
+ )
780
+ )}
781
+
782
+ {/* Following tab */}
783
+ {activeTab === 'following' && (
784
+ filteredFollowing.length === 0 ? (
785
+ <div style={{ ...mutedText, textAlign: 'center', padding: '32px', borderBottom: '1px solid var(--color-border, #d4d4d8)' }}>
786
+ Not following anyone yet.
787
+ </div>
788
+ ) : (
789
+ <>
790
+ <div style={{ ...mutedText, padding: '8px 16px', borderBottom: '1px solid var(--color-border, #d4d4d8)' }}>Following {filteredFollowing.length} agent{filteredFollowing.length !== 1 ? 's' : ''}</div>
791
+ {filteredFollowing.map((f) => (
792
+ <div key={f.auraId} style={{ ...cardStyle, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
793
+ <span style={{ ...accentText, fontSize: '12px' }}>#{f.auraId}</span>
794
+ <span style={mutedText}>{formatTimestamp(f.timestamp)}</span>
795
+ </div>
796
+ ))}
797
+ </>
798
+ )
799
+ )}
800
+
801
+ {/* Sync Status tab */}
802
+ {activeTab === 'status' && (
803
+ syncStatus ? (
804
+ <div style={{ ...cardStyle, borderBottom: '1px solid var(--color-border, #d4d4d8)' }}>
805
+ <div style={{ fontSize: '10px', fontWeight: 600, marginBottom: '12px', fontFamily: 'var(--font-mono, monospace)', textTransform: 'uppercase', letterSpacing: '0.05em', color: 'var(--color-text-muted, #6b7280)' }}>
806
+ Sync Status
807
+ </div>
808
+ <table style={{ width: '100%', fontSize: '12px', borderCollapse: 'collapse' }}>
809
+ <tbody>
810
+ {(Object.entries(syncStatus) as [string, number][]).map(([key, val]) => (
811
+ <tr key={key} style={{ borderTop: '1px solid var(--color-border, #d4d4d8)' }}>
812
+ <td style={{ padding: '6px 0', fontFamily: 'var(--font-mono, monospace)', fontSize: '11px', color: 'var(--color-text-muted, #6b7280)', textTransform: 'uppercase', letterSpacing: '0.03em' }}>{key}</td>
813
+ <td style={{ padding: '6px 0', textAlign: 'right', fontFamily: 'var(--font-mono, monospace)', fontWeight: 600, fontSize: '11px' }}>{val}</td>
814
+ </tr>
815
+ ))}
816
+ </tbody>
817
+ </table>
818
+ </div>
819
+ ) : (
820
+ <div style={{ ...mutedText, textAlign: 'center', padding: '32px', borderBottom: '1px solid var(--color-border, #d4d4d8)' }}>
821
+ No sync data available.
822
+ </div>
823
+ )
824
+ )}
825
+ </>
826
+ )}
827
+ </div>
828
+ </div>
829
+ );
830
+
831
+ return (
832
+ <ViewShell
833
+ sidebar={sidebarContent}
834
+ sidebarFooter={sidebarFooter}
835
+ searchValue={searchValue}
836
+ onSearchChange={setSearchValue}
837
+ searchPlaceholder="Search posts, followers..."
838
+ contentClassName="scrollbar-tyvek"
839
+ >
840
+ {mainContent}
841
+
842
+ <Modal
843
+ isOpen={showPostModal}
844
+ onClose={handleClosePostModal}
845
+ title="Create Post"
846
+ size="sm"
847
+ footer={(
848
+ <div className="flex gap-2 justify-end">
849
+ <Button
850
+ variant="secondary"
851
+ size="sm"
852
+ onClick={handleClosePostModal}
853
+ disabled={posting}
854
+ >
855
+ CANCEL
856
+ </Button>
857
+ <Button
858
+ size="sm"
859
+ loading={posting}
860
+ disabled={!postText.trim()}
861
+ icon={<Send size={12} />}
862
+ onClick={handlePostFromModal}
863
+ >
864
+ POST
865
+ </Button>
866
+ </div>
867
+ )}
868
+ >
869
+ <div className="space-y-3">
870
+ <TextAreaInput
871
+ label="Post"
872
+ placeholder="What's happening?"
873
+ value={postText}
874
+ onChange={(e) => setPostText(e.target.value)}
875
+ onKeyDown={(e) => {
876
+ if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
877
+ e.preventDefault();
878
+ void handlePostFromModal();
879
+ }
880
+ }}
881
+ hint="Cmd/Ctrl+Enter to post"
882
+ autoFocus
883
+ />
884
+ <div style={{ ...mutedText, fontSize: '10px' }}>
885
+ {postText.length > 0 ? `${postText.length} chars` : 'Click Post to publish'}
886
+ </div>
887
+ {postError && (
888
+ <div style={{ fontSize: '11px', color: '#ef4444' }}>{postError}</div>
889
+ )}
890
+ </div>
891
+ </Modal>
892
+
893
+ <CreateAgentModal
894
+ isOpen={showCreateAgent}
895
+ onClose={() => setShowCreateAgent(false)}
896
+ onCreated={(newId) => {
897
+ refreshAgents();
898
+ if (newId) setAgentId(newId);
899
+ }}
900
+ primaryAgentId={primaryAgentId}
901
+ />
902
+ </ViewShell>
903
+ );
904
+ }