aimeat 2.0.0 β†’ 2.1.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 (412) hide show
  1. package/dist/locales/en.json +35 -1
  2. package/dist/locales/fi.json +35 -1
  3. package/dist/public/components/LinkPreview.js +101 -0
  4. package/dist/public/components/NotificationBell.js +18 -1
  5. package/dist/public/css/theme.css +57 -7
  6. package/dist/public/css/views/admin.css +8 -1
  7. package/dist/public/css/views/app-grant.css +18 -0
  8. package/dist/public/css/views/inbox.css +46 -0
  9. package/dist/public/css/views/portfolio.css +46 -0
  10. package/dist/public/css/views/profile.css +29 -4
  11. package/dist/public/js/services/apps.js +19 -3
  12. package/dist/public/js/services/messages-ai-prompts.js +38 -0
  13. package/dist/public/js/services/messages.js +26 -4
  14. package/dist/public/js/services/notebook.js +76 -0
  15. package/dist/public/js/services/unfurl.js +43 -0
  16. package/dist/public/lib/samples/LICENSE.md +11 -0
  17. package/dist/public/lib/samples/bass/A1.mp3 +0 -0
  18. package/dist/public/lib/samples/bass/B2.mp3 +0 -0
  19. package/dist/public/lib/samples/bass/D2.mp3 +0 -0
  20. package/dist/public/lib/samples/bass/E1.mp3 +0 -0
  21. package/dist/public/lib/samples/bass/E3.mp3 +0 -0
  22. package/dist/public/lib/samples/bass/G2.mp3 +0 -0
  23. package/dist/public/lib/samples/epiano/C2.mp3 +0 -0
  24. package/dist/public/lib/samples/epiano/C3.mp3 +0 -0
  25. package/dist/public/lib/samples/epiano/C4.mp3 +0 -0
  26. package/dist/public/lib/samples/epiano/C5.mp3 +0 -0
  27. package/dist/public/lib/samples/epiano/C6.mp3 +0 -0
  28. package/dist/public/lib/samples/epiano/G2.mp3 +0 -0
  29. package/dist/public/lib/samples/epiano/G3.mp3 +0 -0
  30. package/dist/public/lib/samples/epiano/G4.mp3 +0 -0
  31. package/dist/public/lib/samples/epiano/G5.mp3 +0 -0
  32. package/dist/public/lib/samples/guitar-steel/A2.mp3 +0 -0
  33. package/dist/public/lib/samples/guitar-steel/A4.mp3 +0 -0
  34. package/dist/public/lib/samples/guitar-steel/B3.mp3 +0 -0
  35. package/dist/public/lib/samples/guitar-steel/D3.mp3 +0 -0
  36. package/dist/public/lib/samples/guitar-steel/E2.mp3 +0 -0
  37. package/dist/public/lib/samples/guitar-steel/E4.mp3 +0 -0
  38. package/dist/public/lib/samples/guitar-steel/E5.mp3 +0 -0
  39. package/dist/public/lib/samples/guitar-steel/G3.mp3 +0 -0
  40. package/dist/public/lib/samples/organ/C2.mp3 +0 -0
  41. package/dist/public/lib/samples/organ/C3.mp3 +0 -0
  42. package/dist/public/lib/samples/organ/C4.mp3 +0 -0
  43. package/dist/public/lib/samples/organ/C5.mp3 +0 -0
  44. package/dist/public/lib/samples/organ/C6.mp3 +0 -0
  45. package/dist/public/lib/samples/organ/G2.mp3 +0 -0
  46. package/dist/public/lib/samples/organ/G3.mp3 +0 -0
  47. package/dist/public/lib/samples/organ/G4.mp3 +0 -0
  48. package/dist/public/lib/samples/organ/G5.mp3 +0 -0
  49. package/dist/public/lib/samples/strings/C2.mp3 +0 -0
  50. package/dist/public/lib/samples/strings/C3.mp3 +0 -0
  51. package/dist/public/lib/samples/strings/C4.mp3 +0 -0
  52. package/dist/public/lib/samples/strings/C5.mp3 +0 -0
  53. package/dist/public/lib/samples/strings/C6.mp3 +0 -0
  54. package/dist/public/lib/samples/strings/G2.mp3 +0 -0
  55. package/dist/public/lib/samples/strings/G3.mp3 +0 -0
  56. package/dist/public/lib/samples/strings/G4.mp3 +0 -0
  57. package/dist/public/lib/samples/strings/G5.mp3 +0 -0
  58. package/dist/public/lib/samples/trumpet/A3.mp3 +0 -0
  59. package/dist/public/lib/samples/trumpet/A4.mp3 +0 -0
  60. package/dist/public/lib/samples/trumpet/A5.mp3 +0 -0
  61. package/dist/public/lib/samples/trumpet/C4.mp3 +0 -0
  62. package/dist/public/lib/samples/trumpet/C5.mp3 +0 -0
  63. package/dist/public/lib/samples/trumpet/E3.mp3 +0 -0
  64. package/dist/public/lib/samples/trumpet/E4.mp3 +0 -0
  65. package/dist/public/lib/samples/trumpet/E5.mp3 +0 -0
  66. package/dist/public/spa.html +2 -0
  67. package/dist/public/views/app-grant.js +24 -4
  68. package/dist/public/views/portfolio.js +61 -6
  69. package/dist/public/views/profile/apps-tab.js +18 -1
  70. package/dist/public/views/profile/inbox-tab/ai-actions.js +86 -0
  71. package/dist/public/views/profile/inbox-tab/components.js +124 -9
  72. package/dist/public/views/profile/inbox-tab/helpers.js +26 -0
  73. package/dist/public/views/profile/inbox-tab/panels.js +18 -3
  74. package/dist/public/views/profile/inbox-tab/use-thread-ux.js +18 -1
  75. package/dist/public/views/profile/inbox-tab.js +46 -49
  76. package/dist/public/views/profile/landing-page.cards.js +7 -0
  77. package/dist/public/views/profile/landing-page.js +12 -1
  78. package/dist/public/views/profile/skills-tab.js +1 -1
  79. package/dist/public/views/public-workspace-viewer.js +1 -1
  80. package/dist/public/views/start.js +2 -1
  81. package/dist/scripts/build-sdk-libs.d.ts +16 -0
  82. package/dist/scripts/build-sdk-libs.d.ts.map +1 -0
  83. package/dist/scripts/build-sdk-libs.js +112 -0
  84. package/dist/scripts/build-sdk-libs.js.map +1 -0
  85. package/dist/src/cli/connect/auth.d.ts +15 -0
  86. package/dist/src/cli/connect/auth.d.ts.map +1 -1
  87. package/dist/src/cli/connect/auth.js +25 -6
  88. package/dist/src/cli/connect/auth.js.map +1 -1
  89. package/dist/src/cli/connect/keychain.d.ts +9 -0
  90. package/dist/src/cli/connect/keychain.d.ts.map +1 -1
  91. package/dist/src/cli/connect/keychain.js +21 -8
  92. package/dist/src/cli/connect/keychain.js.map +1 -1
  93. package/dist/src/cli/connect/mcp/tools/exchange.d.ts +18 -0
  94. package/dist/src/cli/connect/mcp/tools/exchange.d.ts.map +1 -0
  95. package/dist/src/cli/connect/mcp/tools/exchange.js +170 -0
  96. package/dist/src/cli/connect/mcp/tools/exchange.js.map +1 -0
  97. package/dist/src/cli/connect/mcp/tools/index.d.ts.map +1 -1
  98. package/dist/src/cli/connect/mcp/tools/index.js +2 -0
  99. package/dist/src/cli/connect/mcp/tools/index.js.map +1 -1
  100. package/dist/src/cli/connect/tool-call-defs-exchange.d.ts +3 -0
  101. package/dist/src/cli/connect/tool-call-defs-exchange.d.ts.map +1 -0
  102. package/dist/src/cli/connect/tool-call-defs-exchange.js +146 -0
  103. package/dist/src/cli/connect/tool-call-defs-exchange.js.map +1 -0
  104. package/dist/src/cli/connect/tool-call.d.ts.map +1 -1
  105. package/dist/src/cli/connect/tool-call.js +2 -0
  106. package/dist/src/cli/connect/tool-call.js.map +1 -1
  107. package/dist/src/commerce/session-service.d.ts +14 -0
  108. package/dist/src/commerce/session-service.d.ts.map +1 -1
  109. package/dist/src/commerce/session-service.js +1 -1
  110. package/dist/src/commerce/session-service.js.map +1 -1
  111. package/dist/src/data/app-templates/shells.d.ts +7 -3
  112. package/dist/src/data/app-templates/shells.d.ts.map +1 -1
  113. package/dist/src/data/app-templates/shells.js +10 -6
  114. package/dist/src/data/app-templates/shells.js.map +1 -1
  115. package/dist/src/data/appdev-pitfalls.d.ts +2 -0
  116. package/dist/src/data/appdev-pitfalls.d.ts.map +1 -1
  117. package/dist/src/data/appdev-pitfalls.js +24 -1
  118. package/dist/src/data/appdev-pitfalls.js.map +1 -1
  119. package/dist/src/generated/api-types.d.ts +1565 -9
  120. package/dist/src/generated/api-types.d.ts.map +1 -1
  121. package/dist/src/mcp/annotations.d.ts.map +1 -1
  122. package/dist/src/mcp/annotations.js +20 -0
  123. package/dist/src/mcp/annotations.js.map +1 -1
  124. package/dist/src/mcp/apps.d.ts.map +1 -1
  125. package/dist/src/mcp/apps.js +8 -4
  126. package/dist/src/mcp/apps.js.map +1 -1
  127. package/dist/src/mcp/catalog/definitions/exchange.d.ts +18 -0
  128. package/dist/src/mcp/catalog/definitions/exchange.d.ts.map +1 -0
  129. package/dist/src/mcp/catalog/definitions/exchange.js +182 -0
  130. package/dist/src/mcp/catalog/definitions/exchange.js.map +1 -0
  131. package/dist/src/mcp/catalog/definitions.d.ts.map +1 -1
  132. package/dist/src/mcp/catalog/definitions.js +2 -0
  133. package/dist/src/mcp/catalog/definitions.js.map +1 -1
  134. package/dist/src/mcp/catalog/scopes.d.ts.map +1 -1
  135. package/dist/src/mcp/catalog/scopes.js +22 -0
  136. package/dist/src/mcp/catalog/scopes.js.map +1 -1
  137. package/dist/src/mcp/catalog/surfaces.d.ts.map +1 -1
  138. package/dist/src/mcp/catalog/surfaces.js +14 -0
  139. package/dist/src/mcp/catalog/surfaces.js.map +1 -1
  140. package/dist/src/mcp/exchange-run.d.ts +5 -0
  141. package/dist/src/mcp/exchange-run.d.ts.map +1 -0
  142. package/dist/src/mcp/exchange-run.js +226 -0
  143. package/dist/src/mcp/exchange-run.js.map +1 -0
  144. package/dist/src/mcp/exchange.d.ts +5 -0
  145. package/dist/src/mcp/exchange.d.ts.map +1 -0
  146. package/dist/src/mcp/exchange.js +351 -0
  147. package/dist/src/mcp/exchange.js.map +1 -0
  148. package/dist/src/mcp/extensions.d.ts.map +1 -1
  149. package/dist/src/mcp/extensions.js +11 -0
  150. package/dist/src/mcp/extensions.js.map +1 -1
  151. package/dist/src/mcp/index.d.ts.map +1 -1
  152. package/dist/src/mcp/index.js +6 -2
  153. package/dist/src/mcp/index.js.map +1 -1
  154. package/dist/src/models/app-tool-schemas.d.ts +55 -1
  155. package/dist/src/models/app-tool-schemas.d.ts.map +1 -1
  156. package/dist/src/models/app-tool-schemas.js +20 -1
  157. package/dist/src/models/app-tool-schemas.js.map +1 -1
  158. package/dist/src/models/offer-schemas.d.ts +2 -2
  159. package/dist/src/models/schemas.d.ts +1 -0
  160. package/dist/src/models/schemas.d.ts.map +1 -1
  161. package/dist/src/models/schemas.js +3 -0
  162. package/dist/src/models/schemas.js.map +1 -1
  163. package/dist/src/routes/agents/device-auth.d.ts +4 -0
  164. package/dist/src/routes/agents/device-auth.d.ts.map +1 -1
  165. package/dist/src/routes/agents/device-auth.js +19 -4
  166. package/dist/src/routes/agents/device-auth.js.map +1 -1
  167. package/dist/src/routes/app-grants.d.ts +6 -0
  168. package/dist/src/routes/app-grants.d.ts.map +1 -1
  169. package/dist/src/routes/app-grants.js +35 -2
  170. package/dist/src/routes/app-grants.js.map +1 -1
  171. package/dist/src/routes/apps/fork-manage.d.ts.map +1 -1
  172. package/dist/src/routes/apps/fork-manage.js +24 -1
  173. package/dist/src/routes/apps/fork-manage.js.map +1 -1
  174. package/dist/src/routes/apps/publish.d.ts +2 -0
  175. package/dist/src/routes/apps/publish.d.ts.map +1 -1
  176. package/dist/src/routes/apps/publish.js +27 -1
  177. package/dist/src/routes/apps/publish.js.map +1 -1
  178. package/dist/src/routes/apps-cost.d.ts +25 -0
  179. package/dist/src/routes/apps-cost.d.ts.map +1 -0
  180. package/dist/src/routes/apps-cost.js +107 -0
  181. package/dist/src/routes/apps-cost.js.map +1 -0
  182. package/dist/src/routes/exchange-market.d.ts +28 -0
  183. package/dist/src/routes/exchange-market.d.ts.map +1 -0
  184. package/dist/src/routes/exchange-market.js +454 -0
  185. package/dist/src/routes/exchange-market.js.map +1 -0
  186. package/dist/src/routes/exchange.d.ts +33 -0
  187. package/dist/src/routes/exchange.d.ts.map +1 -0
  188. package/dist/src/routes/exchange.js +395 -0
  189. package/dist/src/routes/exchange.js.map +1 -0
  190. package/dist/src/routes/extensions/entitlement-gate.d.ts +33 -0
  191. package/dist/src/routes/extensions/entitlement-gate.d.ts.map +1 -0
  192. package/dist/src/routes/extensions/entitlement-gate.js +143 -0
  193. package/dist/src/routes/extensions/entitlement-gate.js.map +1 -0
  194. package/dist/src/routes/extensions/manifest.d.ts +9 -0
  195. package/dist/src/routes/extensions/manifest.d.ts.map +1 -1
  196. package/dist/src/routes/extensions/manifest.js +33 -1
  197. package/dist/src/routes/extensions/manifest.js.map +1 -1
  198. package/dist/src/routes/extensions/paywall.d.ts.map +1 -1
  199. package/dist/src/routes/extensions/paywall.js +9 -0
  200. package/dist/src/routes/extensions/paywall.js.map +1 -1
  201. package/dist/src/routes/federation-sync/messaging.d.ts.map +1 -1
  202. package/dist/src/routes/federation-sync/messaging.js +3 -1
  203. package/dist/src/routes/federation-sync/messaging.js.map +1 -1
  204. package/dist/src/routes/ghii/recovery.d.ts +2 -0
  205. package/dist/src/routes/ghii/recovery.d.ts.map +1 -1
  206. package/dist/src/routes/ghii/recovery.js +7 -0
  207. package/dist/src/routes/ghii/recovery.js.map +1 -1
  208. package/dist/src/routes/ghii/register-login.d.ts +4 -0
  209. package/dist/src/routes/ghii/register-login.d.ts.map +1 -1
  210. package/dist/src/routes/ghii/register-login.js +45 -22
  211. package/dist/src/routes/ghii/register-login.js.map +1 -1
  212. package/dist/src/routes/ghii/web-verify.d.ts +5 -0
  213. package/dist/src/routes/ghii/web-verify.d.ts.map +1 -1
  214. package/dist/src/routes/ghii/web-verify.js +24 -3
  215. package/dist/src/routes/ghii/web-verify.js.map +1 -1
  216. package/dist/src/routes/libs/sdk-serve.d.ts +11 -0
  217. package/dist/src/routes/libs/sdk-serve.d.ts.map +1 -0
  218. package/dist/src/routes/libs/sdk-serve.js +53 -0
  219. package/dist/src/routes/libs/sdk-serve.js.map +1 -0
  220. package/dist/src/routes/libs.d.ts +7 -1
  221. package/dist/src/routes/libs.d.ts.map +1 -1
  222. package/dist/src/routes/libs.js +65 -146
  223. package/dist/src/routes/libs.js.map +1 -1
  224. package/dist/src/routes/messages.d.ts +3 -0
  225. package/dist/src/routes/messages.d.ts.map +1 -1
  226. package/dist/src/routes/messages.js +9 -0
  227. package/dist/src/routes/messages.js.map +1 -1
  228. package/dist/src/routes/notifications.d.ts +6 -3
  229. package/dist/src/routes/notifications.d.ts.map +1 -1
  230. package/dist/src/routes/notifications.js +32 -3
  231. package/dist/src/routes/notifications.js.map +1 -1
  232. package/dist/src/routes/organisms/workspace-access.d.ts.map +1 -1
  233. package/dist/src/routes/organisms/workspace-access.js +38 -20
  234. package/dist/src/routes/organisms/workspace-access.js.map +1 -1
  235. package/dist/src/routes/unfurl.d.ts +27 -0
  236. package/dist/src/routes/unfurl.d.ts.map +1 -0
  237. package/dist/src/routes/unfurl.js +180 -0
  238. package/dist/src/routes/unfurl.js.map +1 -0
  239. package/dist/src/routes/upload.d.ts +2 -0
  240. package/dist/src/routes/upload.d.ts.map +1 -1
  241. package/dist/src/routes/upload.js +7 -0
  242. package/dist/src/routes/upload.js.map +1 -1
  243. package/dist/src/routes/webmcp.d.ts +3 -0
  244. package/dist/src/routes/webmcp.d.ts.map +1 -1
  245. package/dist/src/routes/webmcp.js +55 -0
  246. package/dist/src/routes/webmcp.js.map +1 -1
  247. package/dist/src/server-bootstrap/routes-loader.d.ts +1 -0
  248. package/dist/src/server-bootstrap/routes-loader.d.ts.map +1 -1
  249. package/dist/src/server-bootstrap/routes-loader.js +8 -0
  250. package/dist/src/server-bootstrap/routes-loader.js.map +1 -1
  251. package/dist/src/services/agent-face.d.ts +10 -2
  252. package/dist/src/services/agent-face.d.ts.map +1 -1
  253. package/dist/src/services/agent-face.js +10 -4
  254. package/dist/src/services/agent-face.js.map +1 -1
  255. package/dist/src/services/app-tool-interfaces.d.ts +37 -0
  256. package/dist/src/services/app-tool-interfaces.d.ts.map +1 -0
  257. package/dist/src/services/app-tool-interfaces.js +101 -0
  258. package/dist/src/services/app-tool-interfaces.js.map +1 -0
  259. package/dist/src/services/build-app-prompt.d.ts +3 -0
  260. package/dist/src/services/build-app-prompt.d.ts.map +1 -1
  261. package/dist/src/services/build-app-prompt.js +12 -1
  262. package/dist/src/services/build-app-prompt.js.map +1 -1
  263. package/dist/src/services/contacts.d.ts +25 -1
  264. package/dist/src/services/contacts.d.ts.map +1 -1
  265. package/dist/src/services/contacts.js +30 -0
  266. package/dist/src/services/contacts.js.map +1 -1
  267. package/dist/src/services/entitlement-money.d.ts +55 -0
  268. package/dist/src/services/entitlement-money.d.ts.map +1 -0
  269. package/dist/src/services/entitlement-money.js +68 -0
  270. package/dist/src/services/entitlement-money.js.map +1 -0
  271. package/dist/src/services/exchange-market.d.ts +247 -0
  272. package/dist/src/services/exchange-market.d.ts.map +1 -0
  273. package/dist/src/services/exchange-market.js +254 -0
  274. package/dist/src/services/exchange-market.js.map +1 -0
  275. package/dist/src/services/exchange-proposals.d.ts +47 -0
  276. package/dist/src/services/exchange-proposals.d.ts.map +1 -0
  277. package/dist/src/services/exchange-proposals.js +71 -0
  278. package/dist/src/services/exchange-proposals.js.map +1 -0
  279. package/dist/src/services/exchange-work.d.ts +30 -0
  280. package/dist/src/services/exchange-work.d.ts.map +1 -0
  281. package/dist/src/services/exchange-work.js +43 -0
  282. package/dist/src/services/exchange-work.js.map +1 -0
  283. package/dist/src/services/message-send.d.ts.map +1 -1
  284. package/dist/src/services/message-send.js +4 -1
  285. package/dist/src/services/message-send.js.map +1 -1
  286. package/dist/src/services/metered-entitlements.d.ts +203 -0
  287. package/dist/src/services/metered-entitlements.d.ts.map +1 -0
  288. package/dist/src/services/metered-entitlements.js +264 -0
  289. package/dist/src/services/metered-entitlements.js.map +1 -0
  290. package/dist/src/services/notify.d.ts +7 -0
  291. package/dist/src/services/notify.d.ts.map +1 -1
  292. package/dist/src/services/notify.js +35 -0
  293. package/dist/src/services/notify.js.map +1 -1
  294. package/dist/src/services/owner-memory.d.ts +16 -0
  295. package/dist/src/services/owner-memory.d.ts.map +1 -1
  296. package/dist/src/services/owner-memory.js +23 -0
  297. package/dist/src/services/owner-memory.js.map +1 -1
  298. package/dist/src/services/owner-provisioning.d.ts +5 -0
  299. package/dist/src/services/owner-provisioning.d.ts.map +1 -1
  300. package/dist/src/services/owner-provisioning.js +20 -2
  301. package/dist/src/services/owner-provisioning.js.map +1 -1
  302. package/dist/src/storage/providers/postgres-kysely/methods/apps.d.ts +1 -0
  303. package/dist/src/storage/providers/postgres-kysely/methods/apps.d.ts.map +1 -1
  304. package/dist/src/storage/providers/postgres-kysely/methods/apps.js +2 -0
  305. package/dist/src/storage/providers/postgres-kysely/methods/apps.js.map +1 -1
  306. package/dist/src/storage/providers/postgres-kysely/methods/identity.d.ts +1 -0
  307. package/dist/src/storage/providers/postgres-kysely/methods/identity.d.ts.map +1 -1
  308. package/dist/src/storage/providers/postgres-kysely/methods/identity.js +4 -0
  309. package/dist/src/storage/providers/postgres-kysely/methods/identity.js.map +1 -1
  310. package/dist/src/storage/providers/postgres-kysely/migrations/0011_unique_verified_email.sql +25 -0
  311. package/dist/src/storage/providers/sqlite/methods/apps.d.ts +1 -0
  312. package/dist/src/storage/providers/sqlite/methods/apps.d.ts.map +1 -1
  313. package/dist/src/storage/providers/sqlite/methods/apps.js +2 -0
  314. package/dist/src/storage/providers/sqlite/methods/apps.js.map +1 -1
  315. package/dist/src/storage/providers/sqlite/methods/identity-nodes.d.ts +1 -0
  316. package/dist/src/storage/providers/sqlite/methods/identity-nodes.d.ts.map +1 -1
  317. package/dist/src/storage/providers/sqlite/methods/identity-nodes.js +4 -0
  318. package/dist/src/storage/providers/sqlite/methods/identity-nodes.js.map +1 -1
  319. package/dist/src/storage/providers/sqlite/schema.d.ts.map +1 -1
  320. package/dist/src/storage/providers/sqlite/schema.js +20 -0
  321. package/dist/src/storage/providers/sqlite/schema.js.map +1 -1
  322. package/dist/src/storage/repositories/app.repository.d.ts +0 -0
  323. package/dist/src/storage/repositories/app.repository.d.ts.map +1 -1
  324. package/dist/src/storage/repositories/identity.repository.d.ts +6 -0
  325. package/dist/src/storage/repositories/identity.repository.d.ts.map +1 -1
  326. package/dist/src/storage/types/apps.d.ts +1 -0
  327. package/dist/src/storage/types/apps.d.ts.map +1 -1
  328. package/dist/src/storage/types/organisms-federation.d.ts +21 -0
  329. package/dist/src/storage/types/organisms-federation.d.ts.map +1 -1
  330. package/dist/src/utils/app-mobile-lint.d.ts +17 -0
  331. package/dist/src/utils/app-mobile-lint.d.ts.map +1 -0
  332. package/dist/src/utils/app-mobile-lint.js +40 -0
  333. package/dist/src/utils/app-mobile-lint.js.map +1 -0
  334. package/dist/static/app-catalog/_template.html +12 -77
  335. package/dist/static/app-catalog/js/apps-io.js +51 -165
  336. package/dist/static/app-catalog/js/cost.js +100 -0
  337. package/dist/static/app-catalog/js/db.js +23 -93
  338. package/dist/static/app-catalog/js/detail.js +158 -24
  339. package/dist/static/app-catalog/js/favorites.js +71 -0
  340. package/dist/static/app-catalog/js/i18n-data.js +90 -16
  341. package/dist/static/app-catalog/js/main.js +23 -103
  342. package/dist/static/app-catalog/js/migrate.js +88 -0
  343. package/dist/static/app-catalog/js/monetize.js +1 -1
  344. package/dist/static/app-catalog/js/promote.js +70 -0
  345. package/dist/static/app-catalog/js/render.js +26 -139
  346. package/dist/static/app-catalog/js/server-io.js +126 -223
  347. package/dist/static/app-catalog/js/settings.js +11 -204
  348. package/dist/static/app-catalog/styles/app-catalog.css +40 -0
  349. package/dist/static/app-catalog.html +790 -912
  350. package/dist/static/sdk-libs/_core/config.js +52 -0
  351. package/dist/static/sdk-libs/_core/http.js +52 -0
  352. package/dist/static/sdk-libs/_core/namespace.js +33 -0
  353. package/dist/static/sdk-libs/_core/sdk-globals.d.ts +50 -0
  354. package/dist/static/sdk-libs/_core/session.js +66 -0
  355. package/dist/static/sdk-libs/agentface/index.js +102 -0
  356. package/dist/static/sdk-libs/agents/index.js +267 -0
  357. package/dist/static/sdk-libs/ai/index.js +153 -0
  358. package/dist/static/sdk-libs/audio/core.js +111 -0
  359. package/dist/static/sdk-libs/audio/index.js +380 -0
  360. package/dist/static/sdk-libs/audio/instruments.js +369 -0
  361. package/dist/static/sdk-libs/auth/config.js +46 -0
  362. package/dist/static/sdk-libs/auth/crypto.js +166 -0
  363. package/dist/static/sdk-libs/auth/events.js +27 -0
  364. package/dist/static/sdk-libs/auth/i18n.js +64 -0
  365. package/dist/static/sdk-libs/auth/index.js +37 -0
  366. package/dist/static/sdk-libs/auth/modal.js +545 -0
  367. package/dist/static/sdk-libs/auth/pill.js +167 -0
  368. package/dist/static/sdk-libs/auth/session.js +792 -0
  369. package/dist/static/sdk-libs/auth/signup.js +232 -0
  370. package/dist/static/sdk-libs/auth/theme.js +96 -0
  371. package/dist/static/sdk-libs/capabilities/index.js +179 -0
  372. package/dist/static/sdk-libs/commerce/index.js +243 -0
  373. package/dist/static/sdk-libs/data/index.js +188 -0
  374. package/dist/static/sdk-libs/dist/aimeat-agentface.js +91 -0
  375. package/dist/static/sdk-libs/dist/aimeat-agents.js +315 -0
  376. package/dist/static/sdk-libs/dist/aimeat-ai.js +168 -0
  377. package/dist/static/sdk-libs/dist/aimeat-audio.js +979 -0
  378. package/dist/static/sdk-libs/dist/aimeat-auth.js +1831 -0
  379. package/dist/static/sdk-libs/dist/aimeat-capabilities.js +177 -0
  380. package/dist/static/sdk-libs/dist/aimeat-commerce.js +265 -0
  381. package/dist/static/sdk-libs/dist/aimeat-data.js +205 -0
  382. package/dist/static/sdk-libs/dist/aimeat-editor.js +299 -0
  383. package/dist/static/sdk-libs/dist/aimeat-header.js +270 -0
  384. package/dist/static/sdk-libs/dist/aimeat-intake.js +94 -0
  385. package/dist/static/sdk-libs/dist/aimeat-live.js +265 -0
  386. package/dist/static/sdk-libs/dist/aimeat-markdown.js +602 -0
  387. package/dist/static/sdk-libs/dist/aimeat-organism.js +394 -0
  388. package/dist/static/sdk-libs/dist/aimeat-portfolio-standalone.js +90 -0
  389. package/dist/static/sdk-libs/dist/aimeat-social.js +157 -0
  390. package/dist/static/sdk-libs/dist/aimeat-speech.js +300 -0
  391. package/dist/static/sdk-libs/dist/aimeat-storage.js +210 -0
  392. package/dist/static/sdk-libs/dist/aimeat-tunnel.js +383 -0
  393. package/dist/static/sdk-libs/dist/aimeat-wallet.js +118 -0
  394. package/dist/static/sdk-libs/dist/aimeat-webmcp.js +187 -0
  395. package/dist/static/sdk-libs/dist/aimeat-work.js +213 -0
  396. package/dist/static/sdk-libs/dist/aimeat-workflows.js +147 -0
  397. package/dist/static/sdk-libs/editor/index.js +229 -0
  398. package/dist/static/sdk-libs/header/index.js +249 -0
  399. package/dist/static/sdk-libs/intake/index.js +75 -0
  400. package/dist/static/sdk-libs/live/index.js +156 -0
  401. package/dist/static/sdk-libs/markdown/index.js +522 -0
  402. package/dist/static/sdk-libs/organism/index.js +381 -0
  403. package/dist/static/sdk-libs/portfolio-standalone/index.js +78 -0
  404. package/dist/static/sdk-libs/social/index.js +140 -0
  405. package/dist/static/sdk-libs/speech/index.js +321 -0
  406. package/dist/static/sdk-libs/storage/index.js +189 -0
  407. package/dist/static/sdk-libs/tunnel/index.js +428 -0
  408. package/dist/static/sdk-libs/wallet/index.js +115 -0
  409. package/dist/static/sdk-libs/webmcp/index.js +185 -0
  410. package/dist/static/sdk-libs/work/index.js +191 -0
  411. package/dist/static/sdk-libs/workflows/index.js +113 -0
  412. package/package.json +5 -2
@@ -45,8 +45,9 @@ import { h } from 'preact';
45
45
  import { useState, useEffect, useRef } from 'preact/hooks';
46
46
  import htm from 'htm';
47
47
  const html = htm.bind(h);
48
- import { t } from '/js/i18n.js';
48
+ import { t, getLocale } from '/js/i18n.js';
49
49
  import { escHtml } from '/js/utils.js';
50
+ import { listApps } from '/js/services/apps.js';
50
51
  import { NODE_URL, tr, stampCspNonce, getSession } from './portfolio/shared.js';
51
52
  import { PortfolioBuilder } from './portfolio/builder.js';
52
53
 
@@ -56,6 +57,8 @@ function PortfolioViewer({ username, navigate }) {
56
57
  const [data, setData] = useState(null);
57
58
  const [loading, setLoading] = useState(true);
58
59
  const [errMsg, setErrMsg] = useState(null);
60
+ // Apps the owner promoted (their PUBLIC app-catalog.promoted doc), joined to app metadata.
61
+ const [promotedApps, setPromotedApps] = useState([]);
59
62
  const frameRef = useRef(null);
60
63
 
61
64
  // ── Portfolio bridge ──
@@ -144,6 +147,56 @@ function PortfolioViewer({ username, navigate }) {
144
147
  });
145
148
  }, [username]);
146
149
 
150
+ // Load the owner's promoted apps once we know their GHII (data.owner_gaiis[0]). The doc is PUBLIC,
151
+ // read anonymously via /v1/memory/:gaii/:key; app names/icons come from the public /v1/apps list.
152
+ useEffect(() => {
153
+ const ownerGhii = Array.isArray(data?.owner_gaiis) ? data.owner_gaiis[0] : null;
154
+ if (!ownerGhii) { setPromotedApps([]); return undefined; }
155
+ let cancelled = false;
156
+ (async () => {
157
+ try {
158
+ const resp = await fetch(`${NODE_URL}/v1/memory/${encodeURIComponent(ownerGhii)}/${encodeURIComponent('app-catalog.promoted')}?soft=1`);
159
+ const j = await resp.json().catch(() => null);
160
+ const items = (j && j.data && j.data.value && Array.isArray(j.data.value.items)) ? j.data.value.items : [];
161
+ if (!items.length) { if (!cancelled) setPromotedApps([]); return; }
162
+ let byRef = {};
163
+ try {
164
+ const apps = await listApps();
165
+ (apps || []).forEach(a => { byRef[`${a.owner}/${a.filename || a.name}`] = a; });
166
+ } catch { /* names fall back to the filename */ }
167
+ const cards = items.filter(it => it && typeof it.ref === 'string').map(it => {
168
+ const slash = it.ref.indexOf('/');
169
+ const owner = it.ref.slice(0, slash);
170
+ const filename = it.ref.slice(slash + 1);
171
+ const app = byRef[it.ref];
172
+ return {
173
+ ref: it.ref, owner, filename, text: it.text || {},
174
+ name: (app && app.manifest && app.manifest.name) || filename.replace(/\.html?$/i, ''),
175
+ icon: (app && app.manifest && app.manifest.icon) || '\u{1F4E6}',
176
+ };
177
+ });
178
+ if (!cancelled) setPromotedApps(cards);
179
+ } catch { if (!cancelled) setPromotedApps([]); }
180
+ })();
181
+ return () => { cancelled = true; };
182
+ }, [data]);
183
+
184
+ // The "Promoted apps" block β€” the owner's showcased apps with their per-language pitch.
185
+ const promotedBlock = promotedApps.length ? html`
186
+ <div class="portfolio-promoted">
187
+ <h3 class="portfolio-promoted-title">\u{1F4E3} ${tr('portfolio.viewer.promoted', 'Promoted apps')}</h3>
188
+ <div class="portfolio-promoted-grid">
189
+ ${promotedApps.map(p => {
190
+ const pitch = p.text[getLocale()] || p.text.en || p.text.fi || '';
191
+ return html`
192
+ <a class="portfolio-promoted-card" href=${`${NODE_URL}/v1/apps/${encodeURIComponent(p.owner)}/${encodeURIComponent(p.filename)}?mode=inline`} target="_blank" rel="noopener">
193
+ <div class="portfolio-promoted-name">${p.icon} ${escHtml(p.name)}</div>
194
+ ${pitch ? html`<div class="portfolio-promoted-pitch">${escHtml(pitch)}</div>` : ''}
195
+ </a>`;
196
+ })}
197
+ </div>
198
+ </div>` : '';
199
+
147
200
  if (loading) return html`<div class="portfolio-container"><div class="view-loading">Loading portfolio...</div></div>`;
148
201
 
149
202
  if (errMsg || !data) {
@@ -183,6 +236,7 @@ function PortfolioViewer({ username, navigate }) {
183
236
  β›Ά ${tr('portfolio.viewer.fullscreen', 'Fullscreen')}
184
237
  </button>
185
238
  </div>
239
+ ${promotedBlock}
186
240
  <iframe ref=${frameRef} class="portfolio-viewer-frame portfolio-viewer-frame-full"
187
241
  srcdoc=${stampCspNonce(data.portfolio_html)} sandbox="allow-scripts"
188
242
  onLoad=${(e) => postAuthState(e.target.contentWindow)}></iframe>
@@ -190,16 +244,17 @@ function PortfolioViewer({ username, navigate }) {
190
244
  `;
191
245
  }
192
246
 
193
- // No portfolio HTML β€” show basic profile info
247
+ // No portfolio HTML β€” show basic profile info + any promoted apps.
194
248
  return html`
195
249
  <div class="portfolio-container">
196
- <div class="portfolio-not-found">
250
+ <div class="portfolio-profile-head">
197
251
  <h2>${escHtml(data.display_name || username)}</h2>
198
252
  <p>${data.bio ? escHtml(data.bio) : t('portfolio.viewer.notFoundDesc')}</p>
199
- <button class="btn-ghost" style="margin-top:1rem;" onClick=${() => navigate('/v1/portal')}>
200
- ${t('portfolio.viewer.backToPortal')}
201
- </button>
202
253
  </div>
254
+ ${promotedBlock}
255
+ <button class="btn-ghost" style="margin-top:1rem;" onClick=${() => navigate('/v1/portal')}>
256
+ ${t('portfolio.viewer.backToPortal')}
257
+ </button>
203
258
  </div>
204
259
  `;
205
260
  }
@@ -24,11 +24,12 @@ import { h } from 'preact';
24
24
  import { useState, useEffect, useRef, useCallback } from 'preact/hooks';
25
25
  import htm from 'htm';
26
26
  const html = htm.bind(h);
27
- import { t } from '/js/i18n.js';
27
+ import { t, getLocale } from '/js/i18n.js';
28
28
  import { escHtml, handleImgError, timeAgo } from '/js/utils.js';
29
29
  import { Spinner } from './shared.js';
30
30
  import { useConfirm } from '/components/Modal.js';
31
31
  import { listApps, uploadApp, deleteApp, patchApp, deployAppAgent, undeployAppAgent, appAgentStatus } from '/js/services/apps.js';
32
+ import { getMemory } from '/js/services/memory.js';
32
33
  import { listAgents } from '/js/services/agents.js';
33
34
  import { listOrganisms } from '/js/services/organisms.js';
34
35
  import * as skillsService from '/js/services/skills.js';
@@ -289,6 +290,8 @@ export default function AppsTab({ session, showToast, onStats }) {
289
290
  const [editDesc, setEditDesc] = useState('');
290
291
  const [editingAgents, setEditingAgents] = useState(null);
291
292
  const [agentsJson, setAgentsJson] = useState('');
293
+ // Promoted apps (from the owner's public app-catalog.promoted doc), keyed by "owner/filename".
294
+ const [promoted, setPromoted] = useState({});
292
295
 
293
296
  // Reload the app lists when the session becomes available / changes. loadData closes over the
294
297
  // optional onStats prop (not guaranteed memoized), so we key on session only to avoid re-running
@@ -306,6 +309,14 @@ export default function AppsTab({ session, showToast, onStats }) {
306
309
  setAllApps(list);
307
310
  onStats?.({ apps: own.length });
308
311
  } catch { setMyApps([]); setAllApps([]); }
312
+ // Promoted apps: read the owner's own app-catalog.promoted doc (soft = no 404 when unset).
313
+ try {
314
+ const res = await getMemory('app-catalog.promoted', { soft: true });
315
+ const items = res?.data?.value?.items;
316
+ const map = {};
317
+ if (Array.isArray(items)) items.forEach(it => { if (it && it.ref) map[it.ref] = it.text || {}; });
318
+ setPromoted(map);
319
+ } catch { setPromoted({}); }
309
320
  }
310
321
 
311
322
  async function handleUpload(file, description, screenshot, accessCode) {
@@ -480,12 +491,18 @@ export default function AppsTab({ session, showToast, onStats }) {
480
491
  ${a.protected ? html`<span class="badge badge-warn">\u{1F512}</span>` : ''}
481
492
  ${a.parked ? html`<span class="badge badge-dim">\u{1F17F}️ ${t('profile.apps.parkedBadge') || 'Parked'}</span>` : ''}
482
493
  ${a.manifest?.cortex?.agents?.length ? html`<span class="badge badge-info" title=${t('profile.apps.agentBadgeHint') || 'This app ships its own agent β€” deploy it onto your fleet below'}>\u{1F916} ${t('profile.apps.agentBadge') || 'Ships an agent'}</span>` : ''}
494
+ ${promoted[`${a.owner || session.owner}/${a.filename || a.name}`] ? html`<span class="badge badge-success" title=${t('profile.apps.promotedBadgeHint') || 'Promoted on your public profile β€” manage the pitch in the app catalog'}>\u{1F4E3} ${t('profile.apps.promotedBadge') || 'Promoted'}</span>` : ''}
483
495
  ${a.operator_hidden ? html`<span class="badge badge-danger">\u{1F6AB} ${t('profile.apps.operatorHiddenBadge') || 'Moderated by operator: hidden'}</span>` : ''}
484
496
  </div>
485
497
  </div>
486
498
  ${a.operator_hidden ? html`<div class="text-meta-sm mb-half">${t('profile.apps.operatorHiddenHint') || 'An operator has hidden this app from public view. Only you can still see it. Contact the operator to appeal.'}${a.operator_hide_reason ? ' β€” ' + escHtml(a.operator_hide_reason) : ''}</div>` : ''}
487
499
  ${a.parked ? html`<div class="text-meta-sm mb-half">${t('profile.apps.parkedHint') || 'Only you can see and use this β€” hidden from the public catalogue.'}</div>` : ''}
488
500
  ${a.manifest?.description ? html`<div class="text-meta mb-half">${escHtml(a.manifest.description)}</div>` : ''}
501
+ ${(() => {
502
+ const pt = promoted[`${a.owner || session.owner}/${a.filename || a.name}`];
503
+ const txt = pt ? (pt[getLocale()] || pt.en || pt.fi || '') : '';
504
+ return txt ? html`<div class="text-meta-sm mb-half">\u{1F4E3} ${escHtml(txt)}</div>` : '';
505
+ })()}
489
506
  <div class="card-subtitle">
490
507
  <a href="${NODE_URL}/v1/apps/${encodeURIComponent(a.owner || session.owner)}/${encodeURIComponent(a.filename || a.name)}" target="_blank">${t('profile.apps.download')}</a>
491
508
  ${a.size ? ' \u2022 ' + Math.round(a.size / 1024) + ' KB' : ''}
@@ -0,0 +1,86 @@
1
+ /**
2
+ * @file public/views/profile/inbox-tab/ai-actions.js
3
+ * @description Builds the config objects behind the Inbox thread-head AI actions, so InboxTab's handlers
4
+ * stay one-liners (and the file stays under max-file-lines). Two families:
5
+ * - Reply with AI (TARGET-031): buildConversationReplyProps / buildMessageReplyProps β†’ { title, build }
6
+ * for ReplyWithAiPopover (build(mode) β†’ copy/mcp prompt).
7
+ * - Conversation β†’ Notebook: buildConversationNotebookProps β†’ { title, promptText, runServerSummary,
8
+ * parkConversation } for ConversationToNotebookPopover. runServerSummary calls /v1/ai/complete on the
9
+ * owner's own OpenRouter key (vision-aware, up to 8 images); parkConversation files the whole thread.
10
+ * @structure
11
+ * - collectThreadImages(thread, urlMap) β€” [{messageId,id,name,url}] for every image attachment
12
+ * - buildConversationReplyProps({ activeConv, thread, peerName, peerDisplayName })
13
+ * - buildMessageReplyProps({ activeConv, msg, peerName })
14
+ * - buildConversationNotebookProps({ activeConv, thread, urlMap, peerName, title })
15
+ * @usage
16
+ * import { buildConversationNotebookProps } from './inbox-tab/ai-actions.js';
17
+ * setNbConv(buildConversationNotebookProps({ activeConv, thread, urlMap, peerName, title }));
18
+ * @version-history
19
+ * v1.0.0 — 2026-07-19 — Initial: extracted the Reply-with-AI + Conversation→Notebook assembly from inbox-tab.js.
20
+ */
21
+ import { api } from '/js/api.js';
22
+ import { t } from '/js/i18n.js';
23
+ import { parkConversationToNotebook } from '/js/services/notebook.js';
24
+ import { buildConversationReplyPrompt, buildMessageReplyPrompt, buildConversationSummaryPrompt } from '/js/services/messages-ai-prompts.js';
25
+ import { attachKind } from './helpers.js';
26
+
27
+ /** Reply-with-AI props for the whole open conversation (ReplyWithAiPopover: build(mode) β†’ prompt). */
28
+ export function buildConversationReplyProps({ activeConv, thread, peerName, peerDisplayName }) {
29
+ const src = { peerGhii: activeConv.peerGhii, subject: activeConv.subject, conversationId: activeConv.conversationId, thread, peerName };
30
+ return { title: `${t('inbox.ai.replyTo')} ${peerDisplayName}`, build: (mode) => buildConversationReplyPrompt(src, mode) };
31
+ }
32
+
33
+ /** Reply-with-AI props scoped to one message the user pointed at. */
34
+ export function buildMessageReplyProps({ activeConv, msg, peerName }) {
35
+ const src = { peerGhii: activeConv.peerGhii, subject: activeConv.subject, conversationId: activeConv.conversationId, message: msg, peerName };
36
+ return { title: t('inbox.ai.replyToMessage'), build: (mode) => buildMessageReplyPrompt(src, mode) };
37
+ }
38
+
39
+ /** Every image attachment across the loaded thread, resolved to a viewable url (urlMap is keyed
40
+ * `${messageId}::${attachmentId}`). Used both for the vision summary call and the notebook embeds. */
41
+ export function collectThreadImages(msgs, urls) {
42
+ const out = [];
43
+ for (const m of (msgs || [])) {
44
+ for (const a of (m.attachments || [])) {
45
+ if (attachKind(a) !== 'image' || a.expired) continue;
46
+ const url = urls[`${m.id}::${a.id}`];
47
+ if (url) out.push({ messageId: m.id, id: a.id, name: a.name || a.storageKey || 'image', url });
48
+ }
49
+ }
50
+ return out;
51
+ }
52
+
53
+ /**
54
+ * Build the ConversationToNotebookPopover config for the open thread: the summary prompt, plus the
55
+ * server-summary and park callbacks (the popover picks the mode and calls back).
56
+ * @param {object} args
57
+ * @param {{ conversationId?:string, peerGhii?:string, subject?:string }} args.activeConv
58
+ * @param {Array} args.thread
59
+ * @param {Record<string,string>} args.urlMap
60
+ * @param {string} [args.peerName]
61
+ * @param {string} args.title
62
+ */
63
+ export function buildConversationNotebookProps({ activeConv, thread, urlMap, peerName, title }) {
64
+ const src = { peerGhii: activeConv.peerGhii, subject: activeConv.subject, thread, peerName };
65
+ const images = collectThreadImages(thread, urlMap);
66
+ const promptText = buildConversationSummaryPrompt(src);
67
+ // Server-side summary on the owner's own OpenRouter key. Text-only on purpose: the thread's image
68
+ // attachments are JWT-presigned `/v1/download/…` urls that OpenRouter's servers can't fetch (β†’ 502),
69
+ // so we never send them to the model. The images are still captured into the notebook entry below
70
+ // (transcript embeds + source.attachments) for the owner to view; the durable value is the text.
71
+ const runServerSummary = async () => {
72
+ const resp = await api('/v1/ai/complete', {
73
+ method: 'POST',
74
+ body: JSON.stringify({ prompt: promptText }),
75
+ timeoutMs: 1_800_000,
76
+ retries: 0,
77
+ });
78
+ if (resp?.ok === false) throw new Error(resp.error?.message || t('inbox.failed'));
79
+ return resp?.data?.content || '';
80
+ };
81
+ const parkConversation = ({ summary }) => parkConversationToNotebook({
82
+ conv: { conversationId: activeConv.conversationId, peerGhii: activeConv.peerGhii, subject: activeConv.subject },
83
+ thread, peerName, summary, images,
84
+ });
85
+ return { title, promptText, runServerSummary, parkConversation };
86
+ }
@@ -6,6 +6,8 @@
6
6
  * chat.commands), SchedulePanel (own-agent scheduler), and ReplyWithAiPopover (TARGET-031). Each is
7
7
  * self-contained (owns its own hooks). Extracted from inbox-tab.js to satisfy max-file-lines.
8
8
  * @version-history
9
+ * v1.8.0 β€” 2026-07-21 β€” MessageBubble renders link-preview cards under the body (MessageLinkPreviews),
10
+ * gated by the `showLinkPreviews` prop (the persisted thread-head toggle).
9
11
  * v1.0.0 β€” 2026-07-13 β€” Extracted from inbox-tab.js (max-file-lines)
10
12
  * v1.1.0 β€” 2026-07-14 β€” Composer: pasted/dropped images route to the file-attachment path (upload +
11
13
  * shown as an image) instead of Toast UI base64-inlining them into the body (which blew the 50k
@@ -22,6 +24,11 @@
22
24
  * v1.5.0 β€” 2026-07-18 β€” Mobile composer is a plain auto-growing textarea (`mode:'simple'`, no Toast UI
23
25
  * toolbar/Write-Preview/WYSIWYG β€” a phone keyboard + heavy WYSIWYG is miserable); ≀760px opens straight
24
26
  * into it so Toast UI never even loads there. Desktop keeps the rich editor.
27
+ * v1.6.0 β€” 2026-07-19 β€” Composer gets an expand toggle (β€’/‑): enlarges the editor to ~60% of the
28
+ * viewport so long/formatted drafts are fully visible. Rich mode resizes via Toast UI `setHeight`;
29
+ * the markdown fallback + simple textarea grow via the `.inbox-composer--tall` class / lifted cap.
30
+ * v1.7.0 β€” 2026-07-19 β€” ConversationToNotebookPopover: capture a whole thread (with images) into the
31
+ * Notebook via three modes β€” server-side AI summary (owner's key), copy-prompt (own chat), or raw.
25
32
  */
26
33
  import { h } from 'preact';
27
34
  import { useState, useEffect, useCallback, useRef } from 'preact/hooks';
@@ -30,6 +37,7 @@ const html = htm.bind(h);
30
37
  import { t } from '/js/i18n.js';
31
38
  import { escHtml } from '/js/utils.js';
32
39
  import { Markdown } from '/components/Markdown.js';
40
+ import { MessageLinkPreviews } from '/components/LinkPreview.js';
33
41
  import { minidenticon } from '/lib/minidenticons.min.js';
34
42
  import * as schedules from '/js/services/schedules.js';
35
43
  import { MODES } from '/js/services/messages-ai-prompts.js';
@@ -243,7 +251,7 @@ export function PollBuilder({ questions, setQuestions }) {
243
251
  </div>`;
244
252
  }
245
253
 
246
- export function MessageBubble({ msg, mine, urlMap, starred, onStar, onTrack, onPark, onReplyAi, onQuote, quoted, quotedName, onJumpTo, domId, tracked, onOpenMarkdown, answeredWith, onAnswer, submitting }) {
254
+ export function MessageBubble({ msg, mine, urlMap, starred, onStar, onTrack, onPark, onReplyAi, onQuote, quoted, quotedName, onJumpTo, domId, tracked, onOpenMarkdown, answeredWith, onAnswer, submitting, showLinkPreviews }) {
247
255
  const nonInline = (msg.attachments || []).filter(a => !a.inline);
248
256
  const expiredIds = new Set((msg.attachments || []).filter(a => a.expired).map(a => a.id));
249
257
  // urlMap is keyed by `${messageId}::${attachmentId}` because per-message attachment ids (at0, at1…)
@@ -277,6 +285,7 @@ export function MessageBubble({ msg, mine, urlMap, starred, onStar, onTrack, onP
277
285
  <span class="inbox-quote-text">${escHtml(quoteSnippet(quoted.body))}</span>
278
286
  </button>` : null}
279
287
  <div class="inbox-bubble-body"><${Markdown} text=${prepareBody(msg.body, urls, expiredIds)} /></div>
288
+ ${showLinkPreviews ? html`<${MessageLinkPreviews} msg=${msg} />` : null}
280
289
  ${msg.interactive?.role === 'questions' ? (
281
290
  answeredWith
282
291
  ? html`<${InteractiveAnswered} spec=${msg.interactive} answers=${answeredWith.answers || {}} />`
@@ -310,6 +319,8 @@ export function Composer({ recipient, sendLabel, sending, onSend, initialText =
310
319
  const isNarrow = typeof window !== 'undefined' && window.matchMedia?.('(max-width: 760px)').matches;
311
320
  const [mode, setMode] = useState(isNarrow ? 'simple' : 'rich');
312
321
  const [md, setMd] = useState(seeded);
322
+ // Temporarily enlarge the editor (~60% of the viewport) so long/formatted drafts are fully visible.
323
+ const [expanded, setExpanded] = useState(false);
313
324
  const [files, setFiles] = useState([]);
314
325
  const containerRef = useRef(null);
315
326
  const editorRef = useRef(null);
@@ -419,10 +430,26 @@ export function Composer({ recipient, sendLabel, sending, onSend, initialText =
419
430
  // eslint-disable-next-line react-hooks/exhaustive-deps
420
431
  }, [focusNonce]);
421
432
 
422
- // Auto-grow the simple (mobile) textarea to fit its content, capped so it never eats the thread.
423
- const autoGrow = (ta) => { if (!ta) return; ta.style.height = 'auto'; ta.style.height = Math.min(ta.scrollHeight, 132) + 'px'; };
424
- // Size the simple textarea to any seeded draft on mount (and keep it 1 row when empty).
425
- useEffect(() => { if (mode === 'simple') autoGrow(taRef.current); }, [mode]);
433
+ // Resize the Toast UI editor when the expand toggle flips (rich mode sets its own inline height via
434
+ // JS, so a CSS class can't reach it β€” the fallback/simple textareas are sized by `.inbox-composer--tall`
435
+ // in CSS instead). `160px` matches the construction default.
436
+ useEffect(() => {
437
+ if (mode !== 'rich' || !editorRef.current?.setHeight) return;
438
+ const tall = typeof window !== 'undefined' ? Math.round(window.innerHeight * 0.6) : 400;
439
+ try { editorRef.current.setHeight(expanded ? tall + 'px' : '160px'); } catch { /* noop */ }
440
+ }, [expanded, mode]);
441
+
442
+ // Auto-grow the simple (mobile) textarea to fit its content, capped so it never eats the thread. When
443
+ // expanded, the cap lifts to ~60vh so a long draft is fully visible.
444
+ const autoGrow = (ta) => {
445
+ if (!ta) return;
446
+ const cap = expanded && typeof window !== 'undefined' ? Math.round(window.innerHeight * 0.6) : 132;
447
+ ta.style.height = 'auto'; ta.style.height = Math.min(ta.scrollHeight, cap) + 'px';
448
+ };
449
+ // Size the simple textarea to any seeded draft on mount (and keep it 1 row when empty); re-fit when the
450
+ // expand cap changes.
451
+ // eslint-disable-next-line react-hooks/exhaustive-deps
452
+ useEffect(() => { if (mode === 'simple') autoGrow(taRef.current); }, [mode, expanded]);
426
453
 
427
454
  const getText = () => (mode === 'rich' && editorRef.current) ? editorRef.current.getMarkdown() : md;
428
455
  const reset = () => {
@@ -441,7 +468,7 @@ export function Composer({ recipient, sendLabel, sending, onSend, initialText =
441
468
  });
442
469
 
443
470
  return html`
444
- <div class="inbox-composer">
471
+ <div class="inbox-composer ${expanded ? 'inbox-composer--tall' : ''}">
445
472
  ${files.length > 0 ? html`<div class="inbox-file-chips">
446
473
  ${files.map((f, i) => html`<span class="inbox-file-chip" key=${f.name + i}>πŸ“Ž ${escHtml(f.name)}
447
474
  <button class="inbox-bc-chip-x" title=${t('inbox.attachmentRemove')} onClick=${() => removeFile(i)}>βœ•</button></span>`)}
@@ -459,9 +486,13 @@ export function Composer({ recipient, sendLabel, sending, onSend, initialText =
459
486
  <div class="inbox-md-preview"><${Markdown} text=${md} /></div>
460
487
  </div>`}
461
488
  <div class="inbox-composer-bar">
462
- <label class="inbox-attach-btn" title=${t('inbox.attach')}>
463
- πŸ“Ž<input ref=${fileRef} type="file" multiple hidden onChange=${(e) => setFiles(Array.from(e.target.files || []))} />
464
- </label>
489
+ <div class="inbox-bar-left">
490
+ <label class="inbox-attach-btn" title=${t('inbox.attach')}>
491
+ πŸ“Ž<input ref=${fileRef} type="file" multiple hidden onChange=${(e) => setFiles(Array.from(e.target.files || []))} />
492
+ </label>
493
+ <button type="button" class="inbox-attach-btn" title=${expanded ? t('inbox.collapse') : t('inbox.expand')}
494
+ aria-pressed=${expanded} onClick=${() => setExpanded((v) => !v)}>${expanded ? '‑' : '‒'}</button>
495
+ </div>
465
496
  <button class="btn-primary btn-sm" disabled=${sending || !recipient} onClick=${submit}>
466
497
  ${sending ? t('inbox.sending') : sendLabel}
467
498
  </button>
@@ -590,3 +621,87 @@ export function ReplyWithAiPopover({ title, build, onClose, showToast }) {
590
621
  </div>
591
622
  </div>`;
592
623
  }
624
+
625
+ /* ── Conversation β†’ Notebook β€” capture a WHOLE thread (with its images) into the notebook for later
626
+ * filing/enrichment into a workspace. Three modes, all landing in parkConversationToNotebook:
627
+ * ✨ ai β€” summarize server-side with the owner's own OpenRouter key (runServerSummary), edit, park.
628
+ * πŸ“‹ copy β€” copy the summary prompt into the owner's own AI chat, paste the result back, park.
629
+ * πŸ“₯ raw β€” park the whole chain (text + images) as-is; enrich it later in the Notebook.
630
+ * The parent (InboxTab) owns the async work (AI call + park + toasts) via the passed callbacks. ── */
631
+ export function ConversationToNotebookPopover({ title, promptText, runServerSummary, parkConversation, onClose, showToast }) {
632
+ const [mode, setMode] = useState('ai'); // 'ai' | 'copy' | 'raw'
633
+ const [copied, setCopied] = useState(false);
634
+ const [aiSummary, setAiSummary] = useState('');
635
+ const [pasted, setPasted] = useState('');
636
+ const [running, setRunning] = useState(false);
637
+ const [parking, setParking] = useState(false);
638
+
639
+ const copy = async () => {
640
+ try {
641
+ if (navigator.clipboard?.writeText) await navigator.clipboard.writeText(promptText);
642
+ else { const ta = document.createElement('textarea'); ta.value = promptText; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); ta.remove(); }
643
+ setCopied(true); setTimeout(() => setCopied(false), 1800);
644
+ showToast?.(t('inbox.ai.copied'));
645
+ } catch { showToast?.(t('inbox.failed'), true); }
646
+ };
647
+
648
+ const genSummary = async () => {
649
+ setRunning(true);
650
+ try {
651
+ const s = await runServerSummary();
652
+ if (s && s.trim()) setAiSummary(s.trim());
653
+ else showToast?.(t('inbox.notebook.summaryEmpty'), true);
654
+ } catch (e) { showToast?.(e?.message || t('inbox.failed'), true); }
655
+ finally { setRunning(false); }
656
+ };
657
+
658
+ const doPark = async (summary) => {
659
+ setParking(true);
660
+ try { await parkConversation({ summary: summary || '' }); showToast?.(t('inbox.notebook.parked')); onClose(); }
661
+ catch (e) { showToast?.(e?.message || t('inbox.failed'), true); setParking(false); }
662
+ };
663
+
664
+ return html`
665
+ <div class="inbox-ai-overlay" onClick=${onClose}>
666
+ <div class="inbox-ai-modal" onClick=${(e) => e.stopPropagation()}>
667
+ <div class="inbox-ai-head">
668
+ <span class="inbox-ai-title">πŸ““ ${title}</span>
669
+ <button class="btn-ghost btn-sm" onClick=${onClose} title=${t('inbox.close')}>βœ•</button>
670
+ </div>
671
+ <div class="inbox-ai-modes">
672
+ <button class=${`inbox-ai-mode${mode === 'ai' ? ' inbox-ai-mode--on' : ''}`} onClick=${() => setMode('ai')}>✨ ${t('inbox.notebook.modeAi')}</button>
673
+ <button class=${`inbox-ai-mode${mode === 'copy' ? ' inbox-ai-mode--on' : ''}`} onClick=${() => setMode('copy')}>πŸ“‹ ${t('inbox.notebook.modeCopy')}</button>
674
+ <button class=${`inbox-ai-mode${mode === 'raw' ? ' inbox-ai-mode--on' : ''}`} onClick=${() => setMode('raw')}>πŸ“₯ ${t('inbox.notebook.modeRaw')}</button>
675
+ </div>
676
+ ${mode === 'ai' ? html`
677
+ <div class="inbox-ai-hint">${t('inbox.notebook.hintAi')}</div>
678
+ ${!aiSummary ? html`
679
+ <div class="inbox-ai-actions">
680
+ <button class="btn-primary btn-sm" disabled=${running} onClick=${genSummary}>${running ? '… ' + t('inbox.notebook.summarizing') : '✨ ' + t('inbox.notebook.genSummary')}</button>
681
+ </div>`
682
+ : html`
683
+ <textarea class="inbox-ai-text" rows="12" value=${aiSummary} onInput=${(e) => setAiSummary(e.target.value)}></textarea>
684
+ <div class="inbox-ai-actions">
685
+ <button class="btn-ghost btn-sm" disabled=${running} onClick=${genSummary}>${running ? '…' : '↻ ' + t('inbox.notebook.regen')}</button>
686
+ <button class="btn-primary btn-sm" disabled=${parking} onClick=${() => doPark(aiSummary)}>${parking ? '…' : 'πŸ““ ' + t('inbox.notebook.park')}</button>
687
+ </div>`}
688
+ ` : mode === 'copy' ? html`
689
+ <div class="inbox-ai-hint">${t('inbox.notebook.hintCopy')}</div>
690
+ <textarea class="inbox-ai-text" readOnly rows="8" value=${promptText}></textarea>
691
+ <div class="inbox-ai-actions">
692
+ <button class="btn-primary btn-sm" onClick=${copy}>${copied ? 'βœ“ ' + t('inbox.ai.copied') : 'πŸ“‹ ' + t('inbox.ai.copy')}</button>
693
+ </div>
694
+ <div class="inbox-ai-hint">${t('inbox.notebook.pasteHint')}</div>
695
+ <textarea class="inbox-ai-text" rows="8" placeholder=${t('inbox.notebook.pastePh')} value=${pasted} onInput=${(e) => setPasted(e.target.value)}></textarea>
696
+ <div class="inbox-ai-actions">
697
+ <button class="btn-primary btn-sm" disabled=${parking || !pasted.trim()} onClick=${() => doPark(pasted)}>${parking ? '…' : 'πŸ““ ' + t('inbox.notebook.park')}</button>
698
+ </div>
699
+ ` : html`
700
+ <div class="inbox-ai-hint">${t('inbox.notebook.hintRaw')}</div>
701
+ <div class="inbox-ai-actions">
702
+ <button class="btn-primary btn-sm" disabled=${parking} onClick=${() => doPark('')}>${parking ? '…' : 'πŸ“₯ ' + t('inbox.notebook.parkRaw')}</button>
703
+ </div>
704
+ `}
705
+ </div>
706
+ </div>`;
707
+ }
@@ -5,11 +5,37 @@
5
5
  * pixel defense), tracked-state labels, attachment classification, the interactive-answer summary +
6
6
  * poll tally, and the lazy Toast UI editor loader. Extracted from inbox-tab.js to satisfy max-file-lines.
7
7
  * @version-history
8
+ * v1.2.0 β€” 2026-07-21 β€” resolveThreadAttachmentUrls(): resolve non-inline attachment URLs for a loaded
9
+ * thread (reusing the previous conversation's cache), extracted from inbox-tab loadThread.
8
10
  * v1.1.0 β€” 2026-07-17 β€” quoteSnippet(): one-line plain-text excerpt of a message body for reply-quotes.
9
11
  * v1.0.0 β€” 2026-07-13 β€” Extracted from inbox-tab.js (max-file-lines)
10
12
  */
11
13
  import { t, getLocale } from '/js/i18n.js';
12
14
 
15
+ /**
16
+ * Resolve presigned URLs for a thread's non-inline attachments, reusing already-resolved URLs from the
17
+ * SAME conversation's previous cache (a refresh/new message must not re-download every existing image).
18
+ * Keyed by `${messageId}::${attachmentId}` (per-message ids repeat across messages). Inbound resolves the
19
+ * recipient's duplicated local copy; outbound resolves the original. `resolveUrl` = messages.attachmentUrl.
20
+ * Returns { convId, map } to store as the new cache.
21
+ */
22
+ export async function resolveThreadAttachmentUrls(msgs, conversationId, prevCache, resolveUrl) {
23
+ const prev = prevCache?.convId === conversationId ? (prevCache.map || {}) : {};
24
+ const map = {};
25
+ await Promise.all(msgs.flatMap(m => (m.attachments || [])
26
+ .filter(a => !a.inline)
27
+ .map(async a => {
28
+ const uk = `${m.id}::${a.id}`;
29
+ if (prev[uk]) { map[uk] = prev[uk]; return; }
30
+ const key = (a.mode === 'duplicate' && a.localKey) ? a.localKey
31
+ : (m.direction === 'outbound' && a.storageKey) ? a.storageKey : null;
32
+ if (!key) return;
33
+ const u = await resolveUrl(key).catch(() => null);
34
+ if (u) map[uk] = u;
35
+ })));
36
+ return { convId: conversationId, map };
37
+ }
38
+
13
39
  /* Lazy-load the vendored Toast UI Editor (MIT, /lib/toastui/) β€” the same editor the workspace
14
40
  * document space uses, so composing a message feels like editing a document (Markdown⇄WYSIWYG).
15
41
  * ~520KB, so it stays out of the main bundle and loads only when the Inbox composer mounts. */
@@ -6,6 +6,11 @@
6
6
  * (broadcast/poll results). Each is a presentational component driven entirely by props from InboxTab;
7
7
  * the stateful container keeps all hooks. Extracted from inbox-tab.js to satisfy max-file-lines.
8
8
  * @version-history
9
+ * v1.4.0 β€” 2026-07-21 β€” ThreadPanel head: "Show all messages / Last 50" toggle (threadAll/
10
+ * toggleThreadAll), shown once a thread has β‰₯50 messages. Threads default to the full history;
11
+ * the toggle collapses to the newest 50.
12
+ * v1.3.0 β€” 2026-07-21 β€” ThreadPanel: link-preview toggle button in the head (showLinkPreviews /
13
+ * toggleLinkPreviews) + passes the flag down to each MessageBubble.
9
14
  * v1.2.0 β€” 2026-07-18 β€” Clicking ↩ Reply on a bubble now focuses the composer (via `onQuoteReply` +
10
15
  * `composerFocus` bump) so the cursor lands in the input; the βœ• cancel still uses the raw setter.
11
16
  * v1.1.0 β€” 2026-07-17 β€” Reply-to with quote: ThreadPanel resolves each message's `replyToId` to the
@@ -99,8 +104,9 @@ export function ThreadPanel({
99
104
  activeConv, thread, urlMap, important, trackedByMsg, awaitingForConv, awaitingDrafts,
100
105
  schedOpen, setSchedOpen, cmdFill, agentCommands, sending, draftPrefill, prefillNonce, msgsRef,
101
106
  peerDisplay, showToast, toggleImportant, onTrackMsg, onParkMsg, openMessageAi, submitInteractiveAnswers,
102
- setMdViewer, openConversationAi, insertCommand, setCmdFill, cancelTracked, openRecord, startSuggestedReply, doSend,
103
- replyQuote, setReplyQuote, onQuoteReply, composerFocus,
107
+ setMdViewer, openConversationAi, openConversationNotebook, insertCommand, setCmdFill, cancelTracked, openRecord, startSuggestedReply, doSend,
108
+ replyQuote, setReplyQuote, onQuoteReply, composerFocus, showLinkPreviews, toggleLinkPreviews,
109
+ threadAll, toggleThreadAll,
104
110
  }) {
105
111
  let lastDay = '';
106
112
  // Reply-to quotes: resolve a message's `replyToId` to the original within the loaded page (a parent
@@ -142,6 +148,15 @@ export function ThreadPanel({
142
148
  <div class="inbox-sub">${escHtml(activeConv.peerGhii)}</div>
143
149
  </div>
144
150
  ${!viaAgentName ? html`<button class="btn-ghost btn-sm inbox-ai-btn" onClick=${openConversationAi} title=${t('inbox.ai.replyWithAi')}>✨ <span class="inbox-ai-btn-label">${t('inbox.ai.replyWithAi')}</span></button>` : null}
151
+ ${!viaAgentName ? html`<button class="btn-ghost btn-sm inbox-ai-btn" onClick=${openConversationNotebook} title=${t('inbox.notebook.toNotebook')}>πŸ““ <span class="inbox-ai-btn-label">${t('inbox.notebook.toNotebookShort')}</span></button>` : null}
152
+ <button class=${`btn-ghost btn-sm inbox-linkprev-toggle${showLinkPreviews ? ' inbox-linkprev-toggle--on' : ''}`}
153
+ aria-pressed=${!!showLinkPreviews} onClick=${toggleLinkPreviews}
154
+ title=${showLinkPreviews ? t('inbox.linkPreview.hideAll') : t('inbox.linkPreview.showAll')}>
155
+ <span class="inbox-ai-btn-label">${t('inbox.linkPreview.label')}</span></button>
156
+ ${(thread.length >= 50) ? html`<button class=${`btn-ghost btn-sm inbox-linkprev-toggle${threadAll ? ' inbox-linkprev-toggle--on' : ''}`}
157
+ aria-pressed=${!!threadAll} onClick=${toggleThreadAll}
158
+ title=${threadAll ? t('inbox.thread.showRecent') : t('inbox.thread.showAll')}>
159
+ <span class="inbox-ai-btn-label">${threadAll ? t('inbox.thread.showRecent') : t('inbox.thread.showAll')}</span></button>` : null}
145
160
  ${peerIsMyAgent && !viaAgentName ? html`<button class=${`btn-ghost btn-sm inbox-sched-btn${schedOpen ? ' inbox-sched-btn--on' : ''}`}
146
161
  onClick=${() => setSchedOpen(o => !o)} title=${t('inbox.schedTitle')}>πŸ“…</button>` : null}
147
162
  </div>
@@ -159,7 +174,7 @@ export function ThreadPanel({
159
174
  onQuote=${(onQuoteReply && !activeConv.viaAgent) ? onQuoteReply : null}
160
175
  starred=${important.has(m.id)} onStar=${toggleImportant} onTrack=${onTrackMsg} onPark=${onParkMsg} onReplyAi=${openMessageAi} tracked=${trackedByMsg[m.id]}
161
176
  answeredWith=${m.interactive?.role === 'questions' ? answersByQ[m.id] : null}
162
- onAnswer=${submitInteractiveAnswers} submitting=${sending}
177
+ onAnswer=${submitInteractiveAnswers} submitting=${sending} showLinkPreviews=${showLinkPreviews}
163
178
  onOpenMarkdown=${(url, name) => setMdViewer({ url, name })} />`;
164
179
  })}
165
180
  </div>
@@ -7,6 +7,8 @@
7
7
  * Extracted from inbox-tab.js to satisfy max-file-lines.
8
8
  * @usage import { useThreadAutoScroll, useMobileComposerKeyboard } from './inbox-tab/use-thread-ux.js';
9
9
  * @version-history
10
+ * v1.3.0 β€” 2026-07-21 β€” Add useLinkPreviewToggle: persisted global on/off for the message link-preview
11
+ * cards (default ON); the ThreadPanel head button flips it.
10
12
  * v1.2.0 β€” 2026-07-19 β€” useThreadAutoScroll now suppresses the one-time new-message jump while the
11
13
  * composer is focused (near-bottom follow still applies). This lets the inbox reload the open thread
12
14
  * on EVERY 'messages' live-update β€” so incoming messages render immediately even while you're typing
@@ -17,7 +19,22 @@
17
19
  * visualViewport bottom) as `--inbox-avail` instead of the `dvh βˆ’ keyboard` double-count that collapsed
18
20
  * the messenger on Android Chrome; drop the composer scrollIntoView(center) that left a dead gap.
19
21
  */
20
- import { useEffect, useRef } from 'preact/hooks';
22
+ import { useEffect, useRef, useState } from 'preact/hooks';
23
+
24
+ const LINK_PREVIEWS_KEY = 'aimeat.inbox.linkPreviews';
25
+
26
+ /** Persisted global toggle for the link-preview cards (default ON). Returns the flag + a toggler that
27
+ * writes the choice to localStorage. Per-card dismissal is owned separately by /components/LinkPreview.js. */
28
+ export function useLinkPreviewToggle() {
29
+ const [showLinkPreviews, setShow] = useState(() => {
30
+ try { return localStorage.getItem(LINK_PREVIEWS_KEY) !== 'off'; } catch { return true; }
31
+ });
32
+ const toggleLinkPreviews = () => setShow((v) => {
33
+ try { localStorage.setItem(LINK_PREVIEWS_KEY, v ? 'off' : 'on'); } catch { /* quota */ }
34
+ return !v;
35
+ });
36
+ return { showLinkPreviews, toggleLinkPreviews };
37
+ }
21
38
 
22
39
  /** Auto-scroll policy for the open thread: jump to the latest message when a thread is OPENED, follow
23
40
  * when the reader is already near the bottom, and when a genuinely NEW message arrives (last message