@agent-native/core 0.127.1 → 0.128.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 (431) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +92 -0
  3. package/corpus/core/README.md +5 -26
  4. package/corpus/core/docs/content/actions.mdx +47 -4
  5. package/corpus/core/docs/content/native-chat-ui.mdx +7 -0
  6. package/corpus/core/docs/content/organizations-teams-permissions.mdx +109 -3
  7. package/corpus/core/docs/content/template-design-collaboration-and-full-apps.mdx +15 -0
  8. package/corpus/core/docs/content/toolkit-org-team.mdx +72 -12
  9. package/corpus/core/package.json +1 -1
  10. package/corpus/core/src/a2a/artifact-response.ts +36 -29
  11. package/corpus/core/src/action.ts +83 -2
  12. package/corpus/core/src/chat-threads/store.ts +48 -43
  13. package/corpus/core/src/cli/skills-content/design-visual-edit-skill.ts +46 -3
  14. package/corpus/core/src/cli/template-sync.ts +96 -0
  15. package/corpus/core/src/client/AgentPanel.tsx +26 -6
  16. package/corpus/core/src/client/AssistantChat.tsx +63 -7
  17. package/corpus/core/src/client/DefaultSpinner.tsx +29 -0
  18. package/corpus/core/src/client/org/TeamPage.tsx +161 -6
  19. package/corpus/core/src/client/org/hooks.ts +67 -0
  20. package/corpus/core/src/client/org/index.ts +9 -0
  21. package/corpus/core/src/client/transcription/use-live-transcription.ts +62 -10
  22. package/corpus/core/src/db/client.ts +42 -3
  23. package/corpus/core/src/db/ensure-additive-columns.ts +60 -33
  24. package/corpus/core/src/deploy/build.ts +31 -2
  25. package/corpus/core/src/mcp-client/routes.ts +10 -1
  26. package/corpus/core/src/org/app-roles-handlers.ts +152 -0
  27. package/corpus/core/src/org/app-roles.ts +355 -0
  28. package/corpus/core/src/org/index.ts +27 -1
  29. package/corpus/core/src/org/migrations.ts +22 -0
  30. package/corpus/core/src/org/plugin.ts +27 -0
  31. package/corpus/core/src/org/schema.ts +15 -0
  32. package/corpus/core/src/scripts/docs/search.ts +17 -0
  33. package/corpus/core/src/scripts/docs/source-search.ts +10 -0
  34. package/corpus/core/src/server/agent-chat/script-entries.ts +3 -0
  35. package/corpus/core/src/server/agent-chat-plugin.ts +53 -52
  36. package/corpus/core/src/server/builder-design-systems.ts +1 -1
  37. package/corpus/core/src/server/core-routes-plugin.ts +6 -1
  38. package/corpus/core/src/server/framework-request-handler.ts +44 -6
  39. package/corpus/core/src/templates/default/.agents/skills/actions/SKILL.md +32 -0
  40. package/corpus/core/src/templates/default/.agents/skills/adding-a-feature/SKILL.md +7 -0
  41. package/corpus/core/src/templates/default/.agents/skills/security/SKILL.md +31 -0
  42. package/corpus/core/src/templates/headless/.agents/skills/actions/SKILL.md +32 -0
  43. package/corpus/core/src/templates/workspace-core/.agents/skills/actions/SKILL.md +32 -0
  44. package/corpus/core/src/templates/workspace-core/.agents/skills/adding-a-feature/SKILL.md +7 -0
  45. package/corpus/core/src/templates/workspace-core/.agents/skills/authentication/SKILL.md +40 -0
  46. package/corpus/core/src/templates/workspace-core/.agents/skills/security/SKILL.md +31 -0
  47. package/corpus/core/src/templates/workspace-core/.agents/skills/sharing/SKILL.md +4 -0
  48. package/corpus/templates/analytics/.agents/skills/actions/SKILL.md +32 -0
  49. package/corpus/templates/analytics/.agents/skills/security/SKILL.md +31 -0
  50. package/corpus/templates/analytics/.agents/skills/sharing/SKILL.md +4 -0
  51. package/corpus/templates/analytics/actions/data-source-status.ts +21 -12
  52. package/corpus/templates/analytics/app/global.css +7 -7
  53. package/corpus/templates/analytics/app/i18n-data.ts +30 -0
  54. package/corpus/templates/analytics/app/lib/data-source-status.ts +2 -1
  55. package/corpus/templates/analytics/app/pages/DataSources.tsx +54 -10
  56. package/corpus/templates/analytics/changelog/2026-07-28-analytics-pages-load-reliably-with-large-dashboard-workspaces.md +6 -0
  57. package/corpus/templates/analytics/changelog/2026-07-28-dashboard-loading-shimmer-now-flows-smoothly-in-one-directi.md +6 -0
  58. package/corpus/templates/analytics/server/lib/first-party-dashboard-repair.ts +5 -4
  59. package/corpus/templates/analytics/server/plugins/agent-chat.ts +54 -42
  60. package/corpus/templates/analytics/server/plugins/db.ts +1 -18
  61. package/corpus/templates/assets/.agents/skills/actions/SKILL.md +32 -0
  62. package/corpus/templates/assets/.agents/skills/security/SKILL.md +31 -0
  63. package/corpus/templates/assets/.agents/skills/sharing/SKILL.md +4 -0
  64. package/corpus/templates/brain/.agents/skills/actions/SKILL.md +32 -0
  65. package/corpus/templates/brain/.agents/skills/adding-a-feature/SKILL.md +7 -0
  66. package/corpus/templates/brain/.agents/skills/security/SKILL.md +31 -0
  67. package/corpus/templates/brain/.agents/skills/sharing/SKILL.md +4 -0
  68. package/corpus/templates/calendar/.agents/skills/actions/SKILL.md +32 -0
  69. package/corpus/templates/calendar/.agents/skills/security/SKILL.md +31 -0
  70. package/corpus/templates/calendar/.agents/skills/sharing/SKILL.md +4 -0
  71. package/corpus/templates/chat/.agents/skills/actions/SKILL.md +32 -0
  72. package/corpus/templates/chat/.agents/skills/adding-a-feature/SKILL.md +7 -0
  73. package/corpus/templates/chat/.agents/skills/security/SKILL.md +31 -0
  74. package/corpus/templates/chat/.agents/skills/sharing/SKILL.md +4 -0
  75. package/corpus/templates/chat/_gitignore +2 -0
  76. package/corpus/templates/clips/.agents/skills/actions/SKILL.md +32 -0
  77. package/corpus/templates/clips/.agents/skills/security/SKILL.md +31 -0
  78. package/corpus/templates/clips/actions/request-transcript.ts +77 -11
  79. package/corpus/templates/clips/actions/save-browser-transcript.ts +28 -7
  80. package/corpus/templates/clips/app/routes/record.tsx +11 -3
  81. package/corpus/templates/clips/changelog/2026-07-28-browser-recordings-no-longer-save-a-truncated-transcript-as-.md +6 -0
  82. package/corpus/templates/clips/changelog/2026-07-28-browser-recordings-no-longer-stop-transcribing-partway-throu.md +6 -0
  83. package/corpus/templates/clips/changelog/2026-07-28-desktop-full-screen-clips-now-get-an-instant-local-transcrip.md +6 -0
  84. package/corpus/templates/clips/changelog/2026-07-28-long-transcriptions-no-longer-show-a-false-transcription-sto.md +6 -0
  85. package/corpus/templates/clips/changelog/2026-07-28-restarting-for-an-update-now-installs-the-newest-release-ins.md +6 -0
  86. package/corpus/templates/clips/changelog/2026-07-28-the-meeting-recording-pill-now-anchors-to-the-right-edge-of-.md +6 -0
  87. package/corpus/templates/clips/desktop/src/lib/audio-meter.ts +31 -2
  88. package/corpus/templates/clips/desktop/src/lib/recorder.ts +114 -13
  89. package/corpus/templates/clips/desktop/src/lib/updater.ts +12 -4
  90. package/corpus/templates/clips/desktop/src/overlays/live-audio-bars.tsx +40 -15
  91. package/corpus/templates/clips/desktop/src/styles.css +10 -7
  92. package/corpus/templates/clips/desktop/src-tauri/src/recording_indicator.rs +21 -14
  93. package/corpus/templates/clips/shared/transcript-status.ts +10 -0
  94. package/corpus/templates/content/.agents/skills/actions/SKILL.md +32 -0
  95. package/corpus/templates/content/.agents/skills/security/SKILL.md +31 -0
  96. package/corpus/templates/content/.agents/skills/sharing/SKILL.md +4 -0
  97. package/corpus/templates/crm/.agents/skills/sharing/SKILL.md +4 -0
  98. package/corpus/templates/design/.agents/skills/actions/SKILL.md +32 -0
  99. package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +23 -0
  100. package/corpus/templates/design/.agents/skills/security/SKILL.md +31 -0
  101. package/corpus/templates/design/.agents/skills/sharing/SKILL.md +4 -0
  102. package/corpus/templates/design/.agents/skills/visual-edit/SKILL.md +46 -3
  103. package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +7 -5
  104. package/corpus/templates/design/actions/create-file.ts +15 -0
  105. package/corpus/templates/design/actions/generate-design.ts +40 -0
  106. package/corpus/templates/design/actions/present-design-variants.ts +16 -0
  107. package/corpus/templates/design/app/components/design/CanvasContextMenu.tsx +2 -2
  108. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +7 -4
  109. package/corpus/templates/design/app/components/design/keyboard-shortcuts.ts +1 -1
  110. package/corpus/templates/design/app/hooks/useDesignHotkeys.ts +10 -4
  111. package/corpus/templates/design/app/lib/design-ui-events.ts +5 -0
  112. package/corpus/templates/design/app/pages/design-editor/save-failure.ts +7 -1
  113. package/corpus/templates/design/app/root.tsx +18 -1
  114. package/corpus/templates/design/changelog/2026-07-28-generated-designs-with-malformed-html-are-now-rejected-at-sa.md +6 -0
  115. package/corpus/templates/design/changelog/2026-07-28-show-or-hide-design-editing-chrome-with-cmd-on-apple-devices.md +6 -0
  116. package/corpus/templates/design/server/source-workspace.ts +1 -0
  117. package/corpus/templates/design/shared/html-integrity.ts +608 -18
  118. package/corpus/templates/dispatch/.agents/skills/actions/SKILL.md +32 -0
  119. package/corpus/templates/dispatch/.agents/skills/security/SKILL.md +31 -0
  120. package/corpus/templates/dispatch/.agents/skills/sharing/SKILL.md +4 -0
  121. package/corpus/templates/forms/.agents/skills/actions/SKILL.md +32 -0
  122. package/corpus/templates/forms/.agents/skills/security/SKILL.md +31 -0
  123. package/corpus/templates/forms/.agents/skills/sharing/SKILL.md +4 -0
  124. package/corpus/templates/macros/.agents/skills/security/SKILL.md +31 -0
  125. package/corpus/templates/macros/.agents/skills/sharing/SKILL.md +4 -0
  126. package/corpus/templates/mail/.agents/skills/actions/SKILL.md +32 -0
  127. package/corpus/templates/mail/.agents/skills/security/SKILL.md +31 -0
  128. package/corpus/templates/mail/.agents/skills/sharing/SKILL.md +4 -0
  129. package/corpus/templates/plan/.agents/skills/actions/SKILL.md +32 -0
  130. package/corpus/templates/plan/.agents/skills/adding-a-feature/SKILL.md +7 -0
  131. package/corpus/templates/plan/.agents/skills/security/SKILL.md +31 -0
  132. package/corpus/templates/plan/.agents/skills/sharing/SKILL.md +4 -0
  133. package/corpus/templates/slides/.agents/skills/actions/SKILL.md +32 -0
  134. package/corpus/templates/slides/.agents/skills/security/SKILL.md +31 -0
  135. package/corpus/templates/slides/.agents/skills/sharing/SKILL.md +4 -0
  136. package/corpus/templates/slides/app/components/deck/DeckCard.tsx +2 -6
  137. package/corpus/templates/tasks/.agents/skills/actions/SKILL.md +32 -0
  138. package/corpus/templates/tasks/.agents/skills/adding-a-feature/SKILL.md +7 -0
  139. package/corpus/templates/tasks/.agents/skills/security/SKILL.md +31 -0
  140. package/corpus/templates/tasks/.agents/skills/sharing/SKILL.md +4 -0
  141. package/dist/a2a/artifact-response.d.ts.map +1 -1
  142. package/dist/a2a/artifact-response.js +30 -29
  143. package/dist/a2a/artifact-response.js.map +1 -1
  144. package/dist/action.d.ts +36 -0
  145. package/dist/action.d.ts.map +1 -1
  146. package/dist/action.js +37 -2
  147. package/dist/action.js.map +1 -1
  148. package/dist/chat-threads/store.d.ts +12 -0
  149. package/dist/chat-threads/store.d.ts.map +1 -1
  150. package/dist/chat-threads/store.js +48 -42
  151. package/dist/chat-threads/store.js.map +1 -1
  152. package/dist/cli/skills-content/design-visual-edit-skill.d.ts +1 -1
  153. package/dist/cli/skills-content/design-visual-edit-skill.d.ts.map +1 -1
  154. package/dist/cli/skills-content/design-visual-edit-skill.js +46 -3
  155. package/dist/cli/skills-content/design-visual-edit-skill.js.map +1 -1
  156. package/dist/cli/template-sync.d.ts.map +1 -1
  157. package/dist/cli/template-sync.js +88 -0
  158. package/dist/cli/template-sync.js.map +1 -1
  159. package/dist/client/AgentPanel.d.ts +3 -1
  160. package/dist/client/AgentPanel.d.ts.map +1 -1
  161. package/dist/client/AgentPanel.js +11 -9
  162. package/dist/client/AgentPanel.js.map +1 -1
  163. package/dist/client/AssistantChat.d.ts.map +1 -1
  164. package/dist/client/AssistantChat.js +57 -6
  165. package/dist/client/AssistantChat.js.map +1 -1
  166. package/dist/client/DefaultSpinner.d.ts +7 -0
  167. package/dist/client/DefaultSpinner.d.ts.map +1 -1
  168. package/dist/client/DefaultSpinner.js +25 -1
  169. package/dist/client/DefaultSpinner.js.map +1 -1
  170. package/dist/client/org/TeamPage.d.ts +13 -1
  171. package/dist/client/org/TeamPage.d.ts.map +1 -1
  172. package/dist/client/org/TeamPage.js +41 -11
  173. package/dist/client/org/TeamPage.js.map +1 -1
  174. package/dist/client/org/hooks.d.ts +37 -0
  175. package/dist/client/org/hooks.d.ts.map +1 -1
  176. package/dist/client/org/hooks.js +37 -0
  177. package/dist/client/org/hooks.js.map +1 -1
  178. package/dist/client/org/index.d.ts +3 -2
  179. package/dist/client/org/index.d.ts.map +1 -1
  180. package/dist/client/org/index.js +1 -1
  181. package/dist/client/org/index.js.map +1 -1
  182. package/dist/client/transcription/use-live-transcription.d.ts +8 -0
  183. package/dist/client/transcription/use-live-transcription.d.ts.map +1 -1
  184. package/dist/client/transcription/use-live-transcription.js +38 -11
  185. package/dist/client/transcription/use-live-transcription.js.map +1 -1
  186. package/dist/collab/routes.d.ts +1 -1
  187. package/dist/collab/struct-routes.d.ts +1 -1
  188. package/dist/db/client.d.ts +12 -0
  189. package/dist/db/client.d.ts.map +1 -1
  190. package/dist/db/client.js +40 -3
  191. package/dist/db/client.js.map +1 -1
  192. package/dist/db/ensure-additive-columns.d.ts.map +1 -1
  193. package/dist/db/ensure-additive-columns.js +54 -32
  194. package/dist/db/ensure-additive-columns.js.map +1 -1
  195. package/dist/deploy/build.d.ts +1 -0
  196. package/dist/deploy/build.d.ts.map +1 -1
  197. package/dist/deploy/build.js +23 -2
  198. package/dist/deploy/build.js.map +1 -1
  199. package/dist/mcp-client/routes.d.ts +3 -1
  200. package/dist/mcp-client/routes.d.ts.map +1 -1
  201. package/dist/mcp-client/routes.js +8 -2
  202. package/dist/mcp-client/routes.js.map +1 -1
  203. package/dist/notifications/routes.d.ts +1 -1
  204. package/dist/observability/routes.d.ts +3 -3
  205. package/dist/org/app-roles-handlers.d.ts +27 -0
  206. package/dist/org/app-roles-handlers.d.ts.map +1 -0
  207. package/dist/org/app-roles-handlers.js +125 -0
  208. package/dist/org/app-roles-handlers.js.map +1 -0
  209. package/dist/org/app-roles.d.ts +136 -0
  210. package/dist/org/app-roles.d.ts.map +1 -0
  211. package/dist/org/app-roles.js +216 -0
  212. package/dist/org/app-roles.js.map +1 -0
  213. package/dist/org/index.d.ts +4 -1
  214. package/dist/org/index.d.ts.map +1 -1
  215. package/dist/org/index.js +3 -1
  216. package/dist/org/index.js.map +1 -1
  217. package/dist/org/migrations.d.ts.map +1 -1
  218. package/dist/org/migrations.js +22 -0
  219. package/dist/org/migrations.js.map +1 -1
  220. package/dist/org/plugin.d.ts +2 -0
  221. package/dist/org/plugin.d.ts.map +1 -1
  222. package/dist/org/plugin.js +20 -0
  223. package/dist/org/plugin.js.map +1 -1
  224. package/dist/org/schema.d.ts +143 -0
  225. package/dist/org/schema.d.ts.map +1 -1
  226. package/dist/org/schema.js +14 -0
  227. package/dist/org/schema.js.map +1 -1
  228. package/dist/provider-api/actions/custom-provider-registration.d.ts +4 -4
  229. package/dist/provider-api/actions/provider-api.d.ts +12 -12
  230. package/dist/resources/handlers.d.ts +1 -1
  231. package/dist/scripts/docs/search.d.ts.map +1 -1
  232. package/dist/scripts/docs/search.js +15 -0
  233. package/dist/scripts/docs/search.js.map +1 -1
  234. package/dist/scripts/docs/source-search.d.ts +7 -0
  235. package/dist/scripts/docs/source-search.d.ts.map +1 -1
  236. package/dist/scripts/docs/source-search.js +9 -0
  237. package/dist/scripts/docs/source-search.js.map +1 -1
  238. package/dist/server/agent-chat/script-entries.d.ts.map +1 -1
  239. package/dist/server/agent-chat/script-entries.js +3 -0
  240. package/dist/server/agent-chat/script-entries.js.map +1 -1
  241. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  242. package/dist/server/agent-chat-plugin.js +46 -48
  243. package/dist/server/agent-chat-plugin.js.map +1 -1
  244. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  245. package/dist/server/builder-design-systems.js +1 -1
  246. package/dist/server/builder-design-systems.js.map +1 -1
  247. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  248. package/dist/server/core-routes-plugin.js +6 -1
  249. package/dist/server/core-routes-plugin.js.map +1 -1
  250. package/dist/server/framework-request-handler.d.ts.map +1 -1
  251. package/dist/server/framework-request-handler.js +42 -6
  252. package/dist/server/framework-request-handler.js.map +1 -1
  253. package/dist/server/transcribe-voice.d.ts +1 -1
  254. package/dist/templates/chat/.agents/skills/actions/SKILL.md +32 -0
  255. package/dist/templates/chat/.agents/skills/adding-a-feature/SKILL.md +7 -0
  256. package/dist/templates/chat/.agents/skills/security/SKILL.md +31 -0
  257. package/dist/templates/chat/.agents/skills/sharing/SKILL.md +4 -0
  258. package/dist/templates/chat/_gitignore +2 -0
  259. package/dist/templates/default/.agents/skills/actions/SKILL.md +32 -0
  260. package/dist/templates/default/.agents/skills/adding-a-feature/SKILL.md +7 -0
  261. package/dist/templates/default/.agents/skills/security/SKILL.md +31 -0
  262. package/dist/templates/headless/.agents/skills/actions/SKILL.md +32 -0
  263. package/dist/templates/workspace-core/.agents/skills/actions/SKILL.md +32 -0
  264. package/dist/templates/workspace-core/.agents/skills/adding-a-feature/SKILL.md +7 -0
  265. package/dist/templates/workspace-core/.agents/skills/authentication/SKILL.md +40 -0
  266. package/dist/templates/workspace-core/.agents/skills/security/SKILL.md +31 -0
  267. package/dist/templates/workspace-core/.agents/skills/sharing/SKILL.md +4 -0
  268. package/docs/content/actions.mdx +47 -4
  269. package/docs/content/native-chat-ui.mdx +7 -0
  270. package/docs/content/organizations-teams-permissions.mdx +109 -3
  271. package/docs/content/template-design-collaboration-and-full-apps.mdx +15 -0
  272. package/docs/content/toolkit-org-team.mdx +72 -12
  273. package/package.json +1 -1
  274. package/src/a2a/artifact-response.ts +36 -29
  275. package/src/action.ts +83 -2
  276. package/src/chat-threads/store.ts +48 -43
  277. package/src/cli/skills-content/design-visual-edit-skill.ts +46 -3
  278. package/src/cli/template-sync.ts +96 -0
  279. package/src/client/AgentPanel.tsx +26 -6
  280. package/src/client/AssistantChat.tsx +63 -7
  281. package/src/client/DefaultSpinner.tsx +29 -0
  282. package/src/client/org/TeamPage.tsx +161 -6
  283. package/src/client/org/hooks.ts +67 -0
  284. package/src/client/org/index.ts +9 -0
  285. package/src/client/transcription/use-live-transcription.ts +62 -10
  286. package/src/db/client.ts +42 -3
  287. package/src/db/ensure-additive-columns.ts +60 -33
  288. package/src/deploy/build.ts +31 -2
  289. package/src/mcp-client/routes.ts +10 -1
  290. package/src/org/app-roles-handlers.ts +152 -0
  291. package/src/org/app-roles.ts +355 -0
  292. package/src/org/index.ts +27 -1
  293. package/src/org/migrations.ts +22 -0
  294. package/src/org/plugin.ts +27 -0
  295. package/src/org/schema.ts +15 -0
  296. package/src/scripts/docs/search.ts +17 -0
  297. package/src/scripts/docs/source-search.ts +10 -0
  298. package/src/server/agent-chat/script-entries.ts +3 -0
  299. package/src/server/agent-chat-plugin.ts +53 -52
  300. package/src/server/builder-design-systems.ts +1 -1
  301. package/src/server/core-routes-plugin.ts +6 -1
  302. package/src/server/framework-request-handler.ts +44 -6
  303. package/src/templates/chat/.agents/skills/actions/SKILL.md +32 -0
  304. package/src/templates/chat/.agents/skills/adding-a-feature/SKILL.md +7 -0
  305. package/src/templates/chat/.agents/skills/security/SKILL.md +31 -0
  306. package/src/templates/chat/.agents/skills/sharing/SKILL.md +4 -0
  307. package/src/templates/chat/_gitignore +2 -0
  308. package/src/templates/default/.agents/skills/actions/SKILL.md +32 -0
  309. package/src/templates/default/.agents/skills/adding-a-feature/SKILL.md +7 -0
  310. package/src/templates/default/.agents/skills/security/SKILL.md +31 -0
  311. package/src/templates/headless/.agents/skills/actions/SKILL.md +32 -0
  312. package/src/templates/workspace-core/.agents/skills/actions/SKILL.md +32 -0
  313. package/src/templates/workspace-core/.agents/skills/adding-a-feature/SKILL.md +7 -0
  314. package/src/templates/workspace-core/.agents/skills/authentication/SKILL.md +40 -0
  315. package/src/templates/workspace-core/.agents/skills/security/SKILL.md +31 -0
  316. package/src/templates/workspace-core/.agents/skills/sharing/SKILL.md +4 -0
  317. package/corpus/core/src/templates/chat/.agents/skills/actions/SKILL.md +0 -530
  318. package/corpus/core/src/templates/chat/.agents/skills/adding-a-feature/SKILL.md +0 -191
  319. package/corpus/core/src/templates/chat/.agents/skills/agent-native-docs/SKILL.md +0 -115
  320. package/corpus/core/src/templates/chat/.agents/skills/agent-native-toolkit/SKILL.md +0 -177
  321. package/corpus/core/src/templates/chat/.agents/skills/capture-learnings/SKILL.md +0 -89
  322. package/corpus/core/src/templates/chat/.agents/skills/create-skill/SKILL.md +0 -221
  323. package/corpus/core/src/templates/chat/.agents/skills/customizing-agent-native/SKILL.md +0 -220
  324. package/corpus/core/src/templates/chat/.agents/skills/delegate-to-agent/SKILL.md +0 -263
  325. package/corpus/core/src/templates/chat/.agents/skills/feature-flags/SKILL.md +0 -169
  326. package/corpus/core/src/templates/chat/.agents/skills/frontend-design/SKILL.md +0 -174
  327. package/corpus/core/src/templates/chat/.agents/skills/real-time-sync/SKILL.md +0 -232
  328. package/corpus/core/src/templates/chat/.agents/skills/security/SKILL.md +0 -282
  329. package/corpus/core/src/templates/chat/.agents/skills/self-modifying-code/SKILL.md +0 -119
  330. package/corpus/core/src/templates/chat/.agents/skills/shadcn-ui/SKILL.md +0 -123
  331. package/corpus/core/src/templates/chat/.agents/skills/upgrade-agent-native/SKILL.md +0 -122
  332. package/corpus/core/src/templates/chat/.env.example +0 -5
  333. package/corpus/core/src/templates/chat/.ignore +0 -0
  334. package/corpus/core/src/templates/chat/.oxfmtrc.json +0 -8
  335. package/corpus/core/src/templates/chat/AGENTS.md +0 -77
  336. package/corpus/core/src/templates/chat/CHANGELOG.md +0 -10
  337. package/corpus/core/src/templates/chat/DEVELOPING.md +0 -185
  338. package/corpus/core/src/templates/chat/README.md +0 -32
  339. package/corpus/core/src/templates/chat/_gitignore +0 -44
  340. package/corpus/core/src/templates/chat/actions/hello.ts +0 -13
  341. package/corpus/core/src/templates/chat/actions/navigate.ts +0 -41
  342. package/corpus/core/src/templates/chat/actions/run.ts +0 -2
  343. package/corpus/core/src/templates/chat/actions/view-screen.ts +0 -31
  344. package/corpus/core/src/templates/chat/app/components/layout/Header.tsx +0 -60
  345. package/corpus/core/src/templates/chat/app/components/layout/Layout.tsx +0 -188
  346. package/corpus/core/src/templates/chat/app/components/layout/Sidebar.tsx +0 -505
  347. package/corpus/core/src/templates/chat/app/components/ui/button.tsx +0 -1
  348. package/corpus/core/src/templates/chat/app/components/ui/card.tsx +0 -1
  349. package/corpus/core/src/templates/chat/app/components/ui/dropdown-menu.tsx +0 -1
  350. package/corpus/core/src/templates/chat/app/components/ui/input.tsx +0 -1
  351. package/corpus/core/src/templates/chat/app/components/ui/label.tsx +0 -1
  352. package/corpus/core/src/templates/chat/app/components/ui/sheet.tsx +0 -1
  353. package/corpus/core/src/templates/chat/app/components/ui/toolkit-provider.tsx +0 -17
  354. package/corpus/core/src/templates/chat/app/components/ui/tooltip.tsx +0 -1
  355. package/corpus/core/src/templates/chat/app/design-system.ts +0 -3
  356. package/corpus/core/src/templates/chat/app/entry.client.tsx +0 -22
  357. package/corpus/core/src/templates/chat/app/entry.server.tsx +0 -10
  358. package/corpus/core/src/templates/chat/app/global.css +0 -93
  359. package/corpus/core/src/templates/chat/app/hooks/use-navigation-state.ts +0 -93
  360. package/corpus/core/src/templates/chat/app/i18n/ar-SA.ts +0 -68
  361. package/corpus/core/src/templates/chat/app/i18n/de-DE.ts +0 -71
  362. package/corpus/core/src/templates/chat/app/i18n/en-US.ts +0 -70
  363. package/corpus/core/src/templates/chat/app/i18n/es-ES.ts +0 -71
  364. package/corpus/core/src/templates/chat/app/i18n/fr-FR.ts +0 -71
  365. package/corpus/core/src/templates/chat/app/i18n/hi-IN.ts +0 -69
  366. package/corpus/core/src/templates/chat/app/i18n/index.ts +0 -34
  367. package/corpus/core/src/templates/chat/app/i18n/ja-JP.ts +0 -70
  368. package/corpus/core/src/templates/chat/app/i18n/ko-KR.ts +0 -70
  369. package/corpus/core/src/templates/chat/app/i18n/pt-BR.ts +0 -71
  370. package/corpus/core/src/templates/chat/app/i18n/zh-CN.ts +0 -66
  371. package/corpus/core/src/templates/chat/app/i18n/zh-TW.ts +0 -66
  372. package/corpus/core/src/templates/chat/app/i18n-data.ts +0 -556
  373. package/corpus/core/src/templates/chat/app/lib/agent-page.tsx +0 -49
  374. package/corpus/core/src/templates/chat/app/lib/app-config.ts +0 -11
  375. package/corpus/core/src/templates/chat/app/lib/tab-id.ts +0 -1
  376. package/corpus/core/src/templates/chat/app/lib/utils.ts +0 -1
  377. package/corpus/core/src/templates/chat/app/root.tsx +0 -173
  378. package/corpus/core/src/templates/chat/app/routes/_index.tsx +0 -94
  379. package/corpus/core/src/templates/chat/app/routes/agent.tsx +0 -24
  380. package/corpus/core/src/templates/chat/app/routes/chat.$threadId.tsx +0 -1
  381. package/corpus/core/src/templates/chat/app/routes/database.tsx +0 -17
  382. package/corpus/core/src/templates/chat/app/routes/extensions.$id.$slug.tsx +0 -2
  383. package/corpus/core/src/templates/chat/app/routes/extensions.$id.tsx +0 -11
  384. package/corpus/core/src/templates/chat/app/routes/extensions._index.tsx +0 -11
  385. package/corpus/core/src/templates/chat/app/routes/extensions.tsx +0 -5
  386. package/corpus/core/src/templates/chat/app/routes/observability.tsx +0 -19
  387. package/corpus/core/src/templates/chat/app/routes/settings.tsx +0 -89
  388. package/corpus/core/src/templates/chat/app/routes/team.tsx +0 -11
  389. package/corpus/core/src/templates/chat/app/routes.ts +0 -4
  390. package/corpus/core/src/templates/chat/app/vite-env.d.ts +0 -6
  391. package/corpus/core/src/templates/chat/changelog/2026-06-24-a-new-settings-page-gives-quick-access-to-language-workspace.md +0 -6
  392. package/corpus/core/src/templates/chat/changelog/2026-06-24-added-a-language-picker-and-localized-app-chrome-for-support.md +0 -6
  393. package/corpus/core/src/templates/chat/changelog/2026-06-27-traditional-chinese-copy-uses-taiwan-terminology.md +0 -6
  394. package/corpus/core/src/templates/chat/changelog/2026-06-28-left-sidebar-collapse-motion-and-footer-chrome-are-quieter.md +0 -6
  395. package/corpus/core/src/templates/chat/changelog/2026-06-29-chat-layouts-adapt-when-the-agent-sidebar-is-open.md +0 -6
  396. package/corpus/core/src/templates/chat/changelog/2026-07-08-settings-are-cleaner-and-searchable.md +0 -5
  397. package/corpus/core/src/templates/chat/changelog/2026-07-10-chat-now-makes-ai-connection-setup-clear-without-shifting-th.md +0 -6
  398. package/corpus/core/src/templates/chat/changelog/2026-07-13-a-full-agent-page-now-brings-context-files-connections-jobs-.md +0 -6
  399. package/corpus/core/src/templates/chat/changelog/2026-07-14-chat-opens-reliably-on-hosted-deployments-instead-of-failing.md +0 -6
  400. package/corpus/core/src/templates/chat/changelog/2026-07-14-fixed-chat-template-startup-with-older-core-versions.md +0 -6
  401. package/corpus/core/src/templates/chat/changelog/2026-07-15-chat-navigation-focuses-on-chat-and-agent.md +0 -6
  402. package/corpus/core/src/templates/chat/changelog/2026-07-17-the-agent-chat-sidebar-stays-closed-until-you-open-it-or-sta.md +0 -6
  403. package/corpus/core/src/templates/chat/changelog/2026-07-22-compact-sidebar-footer.md +0 -6
  404. package/corpus/core/src/templates/chat/changelog/2026-07-22-full-page-chat-keeps-the-active-conversation-when-moving-to-.md +0 -6
  405. package/corpus/core/src/templates/chat/changelog/2026-07-22-manage-agent-navigation-now-uses-the-connected-nodes-icon.md +0 -6
  406. package/corpus/core/src/templates/chat/changelog/2026-07-22-recent-chats-are-easier-to-scan-and-expand.md +0 -6
  407. package/corpus/core/src/templates/chat/changelog/2026-07-23-full-page-chat-is-better-centered-with-quieter-chat-history-.md +0 -6
  408. package/corpus/core/src/templates/chat/changelog/2026-07-24-borderless-secondary-surfaces.md +0 -6
  409. package/corpus/core/src/templates/chat/changelog/2026-07-24-sidebar-footer-controls-follow-a-consistent-order.md +0 -6
  410. package/corpus/core/src/templates/chat/changelog/2026-07-25-settings-navigation-now-keeps-manage-agent-as-a-dedicated-li.md +0 -6
  411. package/corpus/core/src/templates/chat/components.json +0 -20
  412. package/corpus/core/src/templates/chat/learnings.defaults.md +0 -5
  413. package/corpus/core/src/templates/chat/netlify.toml +0 -11
  414. package/corpus/core/src/templates/chat/package.json +0 -92
  415. package/corpus/core/src/templates/chat/public/agent-native-icon-dark.svg +0 -10
  416. package/corpus/core/src/templates/chat/public/agent-native-icon-light.svg +0 -10
  417. package/corpus/core/src/templates/chat/public/agent-native-logo-dark.svg +0 -21
  418. package/corpus/core/src/templates/chat/public/agent-native-logo-light.svg +0 -21
  419. package/corpus/core/src/templates/chat/public/favicon.svg +0 -1
  420. package/corpus/core/src/templates/chat/public/icon-180.svg +0 -1
  421. package/corpus/core/src/templates/chat/public/icon-192.svg +0 -1
  422. package/corpus/core/src/templates/chat/public/icon-512.svg +0 -1
  423. package/corpus/core/src/templates/chat/public/manifest.json +0 -21
  424. package/corpus/core/src/templates/chat/react-router.config.ts +0 -7
  425. package/corpus/core/src/templates/chat/server/middleware/auth.ts +0 -15
  426. package/corpus/core/src/templates/chat/server/plugins/agent-chat.ts +0 -21
  427. package/corpus/core/src/templates/chat/server/plugins/auth.ts +0 -17
  428. package/corpus/core/src/templates/chat/server/routes/[...page].get.ts +0 -5
  429. package/corpus/core/src/templates/chat/ssr-entry.ts +0 -15
  430. package/corpus/core/src/templates/chat/tsconfig.json +0 -21
  431. package/corpus/core/src/templates/chat/vite.config.ts +0 -19
@@ -333,8 +333,16 @@ function activeRunStuckThresholdMs(runInfo: ActiveRunLookup): number {
333
333
  }
334
334
 
335
335
  function activeRunLooksStale(runInfo: ActiveRunLookup): boolean {
336
+ // A run killed before it emitted anything has no `lastProgressAt`. Falling
337
+ // back to the heartbeat keeps that case from reading as perpetually fresh —
338
+ // "never reported progress" is the worst case, not an exemption from the
339
+ // staleness check.
336
340
  const lastProgressAt =
337
- typeof runInfo.lastProgressAt === "number" ? runInfo.lastProgressAt : null;
341
+ typeof runInfo.lastProgressAt === "number"
342
+ ? runInfo.lastProgressAt
343
+ : typeof runInfo.heartbeatAt === "number"
344
+ ? runInfo.heartbeatAt
345
+ : null;
338
346
  const nowMs =
339
347
  typeof runInfo.serverNow === "number" ? runInfo.serverNow : Date.now();
340
348
  const thresholdMs = activeRunStuckThresholdMs(runInfo);
@@ -345,6 +353,16 @@ function activeRunLooksStale(runInfo: ActiveRunLookup): boolean {
345
353
  );
346
354
  }
347
355
 
356
+ /**
357
+ * The stored active run is what keeps the "Thinking…" spinner up across a
358
+ * reload, and the `/runs/active` probe is the only path that clears it. A 5xx
359
+ * or a network drop there means "couldn't determine", NOT "still running" —
360
+ * collapsing the two is what strands the spinner forever when the server is
361
+ * briefly unreachable, because no caller reschedules the probe. Retry inside
362
+ * the call, then give up loudly.
363
+ */
364
+ const ACTIVE_RUN_PROBE_RETRY_DELAYS_MS = [400, 1200];
365
+
348
366
  /**
349
367
  * Decide whether a reconnect should give up with "no progress". The signal is
350
368
  * *time since the last streamed event*, NOT total reconnect duration: a healthy
@@ -3440,12 +3458,50 @@ const AssistantChatInner = forwardRef<
3440
3458
  if (isRuntimeRunningRef.current || isAutoResumingRef.current) {
3441
3459
  return false;
3442
3460
  }
3461
+ const storedActiveRun = getActiveRun();
3462
+ let runRes: Response | null = null;
3463
+ for (let attempt = 0; ; attempt += 1) {
3464
+ try {
3465
+ const res = await fetch(
3466
+ `${apiUrl}/runs/active?threadId=${encodeURIComponent(threadId)}`,
3467
+ );
3468
+ if (res.ok) {
3469
+ runRes = res;
3470
+ break;
3471
+ }
3472
+ } catch {
3473
+ // Fall through to the retry/give-up path below.
3474
+ }
3475
+ const retryDelayMs = ACTIVE_RUN_PROBE_RETRY_DELAYS_MS[attempt];
3476
+ if (retryDelayMs === undefined) break;
3477
+ await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
3478
+ }
3479
+
3480
+ if (!runRes) {
3481
+ // Still unreachable. Only a stored run for this thread is holding the
3482
+ // spinner up, so that is also the only case worth interrupting the user
3483
+ // for — drop it and say why, rather than leaving "Thinking…" forever.
3484
+ if (storedActiveRun?.threadId === threadId) {
3485
+ clearActiveRunIfMatches(threadId, storedActiveRun.runId);
3486
+ window.dispatchEvent(
3487
+ new CustomEvent("agent-chat:run-error", {
3488
+ detail: {
3489
+ message:
3490
+ "Couldn't reach the server to check whether the agent is still working. Send your message again to retry.",
3491
+ errorCode: "run_status_unavailable",
3492
+ recoverable: true,
3493
+ ...(storedActiveRun.runId
3494
+ ? { runId: storedActiveRun.runId }
3495
+ : {}),
3496
+ ...(tabId ? { tabId } : {}),
3497
+ },
3498
+ }),
3499
+ );
3500
+ }
3501
+ return false;
3502
+ }
3503
+
3443
3504
  try {
3444
- const storedActiveRun = getActiveRun();
3445
- const runRes = await fetch(
3446
- `${apiUrl}/runs/active?threadId=${encodeURIComponent(threadId)}`,
3447
- );
3448
- if (!runRes.ok) return false;
3449
3505
  const runInfo = (await runRes.json()) as ActiveRunLookup;
3450
3506
  if (
3451
3507
  !runInfo.active ||
@@ -3464,7 +3520,7 @@ const AssistantChatInner = forwardRef<
3464
3520
  } catch {
3465
3521
  return false;
3466
3522
  }
3467
- }, [apiUrl, refreshThreadFromServer, startReconnectToRun, threadId]);
3523
+ }, [apiUrl, refreshThreadFromServer, startReconnectToRun, tabId, threadId]);
3468
3524
 
3469
3525
  useEffect(() => {
3470
3526
  if (!threadId || !isNewThread) return;
@@ -2,14 +2,23 @@
2
2
  * Full-screen loading spinner rendered during SSR and initial hydration.
3
3
  * Uses inline SVG + styles because Tailwind may not be loaded yet on the server.
4
4
  * Respects the user's OS color scheme so dark-mode users don't get a white flash.
5
+ *
6
+ * The stall hint is revealed by a pure-CSS `animation-delay`, never a timer:
7
+ * the states that strand a user here (hydration never runs, the route module
8
+ * 404s, a cold dev-server compile) are exactly the states where no JS of ours
9
+ * executes, so a `setTimeout` fallback would never fire. A featureless spinner
10
+ * is indistinguishable from a blank screen, and reads as "the app is broken"
11
+ * rather than "look at the terminal" — that mis-read is what this text buys.
5
12
  */
6
13
  export function DefaultSpinner() {
7
14
  return (
8
15
  <div
9
16
  style={{
10
17
  display: "flex",
18
+ flexDirection: "column",
11
19
  alignItems: "center",
12
20
  justifyContent: "center",
21
+ gap: 16,
13
22
  height: "100vh",
14
23
  width: "100%",
15
24
  }}
@@ -29,8 +38,28 @@ export function DefaultSpinner() {
29
38
  >
30
39
  <path d="M21 12a9 9 0 1 1-6.219-8.56" />
31
40
  </svg>
41
+ <p className="an-stall-hint">
42
+ Still loading. A first run compiles dependencies and can take a minute —
43
+ if it does not finish, check the terminal running the dev server for
44
+ errors.
45
+ </p>
32
46
  <style>{`
33
47
  @keyframes an-spin { to { transform: rotate(360deg) } }
48
+ @keyframes an-stall-in { to { opacity: 0.6 } }
49
+ .an-stall-hint {
50
+ opacity: 0;
51
+ margin: 0;
52
+ max-width: 32rem;
53
+ padding: 0 1.5rem;
54
+ text-align: center;
55
+ font-size: 0.875rem;
56
+ line-height: 1.5;
57
+ font-family: ui-sans-serif, system-ui, sans-serif;
58
+ animation: an-stall-in 0.4s ease-out 10s forwards;
59
+ }
60
+ @media (prefers-reduced-motion: reduce) {
61
+ .an-stall-hint { animation-duration: 0s }
62
+ }
34
63
  html {
35
64
  background: hsl(var(--background, 0 0% 100%));
36
65
  color: hsl(var(--foreground, 240 10% 3.9%));
@@ -56,6 +56,9 @@ import {
56
56
  type ReactNode,
57
57
  } from "react";
58
58
 
59
+ // Type-only: erased at build time, so declaring app roles pulls no server or
60
+ // database code into the browser bundle.
61
+ import type { AppRolesDescriptor } from "../../org/app-roles.js";
59
62
  import type { DomainMatchOrg } from "../../org/types.js";
60
63
  import {
61
64
  Tooltip,
@@ -82,6 +85,8 @@ import {
82
85
  useSetA2ASecret,
83
86
  useSyncA2ASecret,
84
87
  useJoinByDomain,
88
+ useAppRoles,
89
+ useSetAppMemberRole,
85
90
  type InviteRole,
86
91
  type SyncA2ASecretResult,
87
92
  } from "./hooks.js";
@@ -127,6 +132,17 @@ export interface TeamPageProps {
127
132
  * tweak page width.
128
133
  */
129
134
  className?: string;
135
+ /**
136
+ * Opt in to an app-role column on the members table, using the same
137
+ * descriptor the app passes to `defineAppRoles`. Pass it explicitly rather
138
+ * than letting the page discover registered apps: a workspace can host
139
+ * several, and a members table that silently grows a column when some
140
+ * unrelated module registers itself is a surprise, not a feature.
141
+ *
142
+ * Only org owners/admins can change assignments; everyone else sees the
143
+ * column read-only.
144
+ */
145
+ appRoles?: AppRolesDescriptor;
130
146
  }
131
147
 
132
148
  function RoleIcon({ role }: { role: string }) {
@@ -383,7 +399,7 @@ interface PendingInviteListItem {
383
399
  role: string;
384
400
  }
385
401
 
386
- function MembersCard() {
402
+ function MembersCard({ appRoles }: { appRoles?: AppRolesDescriptor }) {
387
403
  const t = useT();
388
404
  const { data: org } = useOrg();
389
405
  const { data: membersData, isLoading: isLoadingMembers } = useOrgMembers();
@@ -457,6 +473,7 @@ function MembersCard() {
457
473
  isLoadingMembers={isLoadingMembers}
458
474
  currentUserEmail={org.email}
459
475
  currentUserRole={org.role ?? null}
476
+ appRoles={appRoles}
460
477
  />
461
478
 
462
479
  {isOwner && <DangerZoneCard orgName={org.orgName ?? ""} />}
@@ -470,16 +487,26 @@ function MembersTableCard({
470
487
  isLoadingMembers,
471
488
  currentUserEmail,
472
489
  currentUserRole,
490
+ appRoles,
473
491
  }: {
474
492
  members: MemberListItem[];
475
493
  pendingInvites: PendingInviteListItem[];
476
494
  isLoadingMembers: boolean;
477
495
  currentUserEmail: string;
478
496
  currentUserRole: string | null;
497
+ appRoles?: AppRolesDescriptor;
479
498
  }) {
480
499
  const t = useT();
481
500
  const [showInviteForm, setShowInviteForm] = useState(false);
482
501
  const canInvite = currentUserRole === "owner" || currentUserRole === "admin";
502
+ const { data: appRoleData } = useAppRoles(appRoles?.appId);
503
+ const appRoleByEmail = new Map(
504
+ (appRoleData?.assignments ?? []).map((a) => [
505
+ a.email.toLowerCase(),
506
+ a.role,
507
+ ]),
508
+ );
509
+ const columnCount = appRoles ? 5 : 4;
483
510
 
484
511
  return (
485
512
  <section className="rounded-lg border border-border bg-card">
@@ -516,6 +543,9 @@ function MembersTableCard({
516
543
  <TableRow>
517
544
  <TableHead>{t("org.member")}</TableHead>
518
545
  <TableHead>{t("org.role")}</TableHead>
546
+ {appRoles && (
547
+ <TableHead>{appRoles.label ?? appRoles.appId}</TableHead>
548
+ )}
519
549
  <TableHead>{t("org.status")}</TableHead>
520
550
  <TableHead className="text-end">{t("org.actions")}</TableHead>
521
551
  </TableRow>
@@ -524,7 +554,7 @@ function MembersTableCard({
524
554
  {isLoadingMembers && members.length === 0 ? (
525
555
  [0, 1, 2].map((i) => (
526
556
  <TableRow key={i}>
527
- <TableCell colSpan={4}>
557
+ <TableCell colSpan={columnCount}>
528
558
  <div
529
559
  className="h-3.5 rounded bg-muted animate-pulse"
530
560
  style={{ width: `${180 + i * 48}px` }}
@@ -535,7 +565,7 @@ function MembersTableCard({
535
565
  ) : members.length === 0 && pendingInvites.length === 0 ? (
536
566
  <TableRow>
537
567
  <TableCell
538
- colSpan={4}
568
+ colSpan={columnCount}
539
569
  className="py-8 text-center text-sm text-muted-foreground"
540
570
  >
541
571
  {t("org.noMembers")}
@@ -550,10 +580,17 @@ function MembersTableCard({
550
580
  role={m.role}
551
581
  isCurrentUser={m.email === currentUserEmail}
552
582
  currentUserRole={currentUserRole}
583
+ appRoles={appRoles}
584
+ appRole={appRoleByEmail.get(m.email.toLowerCase()) ?? null}
585
+ canManageAppRoles={Boolean(appRoleData?.canManage)}
553
586
  />
554
587
  ))}
555
588
  {pendingInvites.map((inv) => (
556
- <PendingInviteRow key={inv.id} invite={inv} />
589
+ <PendingInviteRow
590
+ key={inv.id}
591
+ invite={inv}
592
+ hasAppRoleColumn={Boolean(appRoles)}
593
+ />
557
594
  ))}
558
595
  </>
559
596
  )}
@@ -665,7 +702,13 @@ function RoleBadge({ role }: { role: string }) {
665
702
  );
666
703
  }
667
704
 
668
- function PendingInviteRow({ invite }: { invite: PendingInviteListItem }) {
705
+ function PendingInviteRow({
706
+ invite,
707
+ hasAppRoleColumn,
708
+ }: {
709
+ invite: PendingInviteListItem;
710
+ hasAppRoleColumn?: boolean;
711
+ }) {
669
712
  const t = useT();
670
713
  return (
671
714
  <TableRow className="opacity-70">
@@ -675,6 +718,13 @@ function PendingInviteRow({ invite }: { invite: PendingInviteListItem }) {
675
718
  <TableCell>
676
719
  <RoleBadge role={invite.role} />
677
720
  </TableCell>
721
+ {/* App roles hang off membership, so there is nothing to assign until the
722
+ invitation is accepted. */}
723
+ {hasAppRoleColumn && (
724
+ <TableCell>
725
+ <span className="text-muted-foreground">-</span>
726
+ </TableCell>
727
+ )}
678
728
  <TableCell>
679
729
  <span className="rounded border border-border px-1.5 py-0.5 text-[10px] text-muted-foreground">
680
730
  {t("org.invited")}
@@ -685,16 +735,112 @@ function PendingInviteRow({ invite }: { invite: PendingInviteListItem }) {
685
735
  );
686
736
  }
687
737
 
738
+ /** Sentinel for "clear the assignment" — Select cannot carry an empty value. */
739
+ const UNASSIGNED = "__unassigned__";
740
+
741
+ function AppRoleCell({
742
+ email,
743
+ appRoles,
744
+ appRole,
745
+ canManage,
746
+ }: {
747
+ email: string;
748
+ appRoles: AppRolesDescriptor;
749
+ appRole: string | null;
750
+ canManage: boolean;
751
+ }) {
752
+ const [editing, setEditing] = useState(false);
753
+ const setAppRole = useSetAppMemberRole(appRoles.appId);
754
+ const labelFor = (r: string) => appRoles.roleLabels?.[r] ?? r;
755
+
756
+ if (editing) {
757
+ return (
758
+ <TableCell>
759
+ <Select
760
+ defaultOpen
761
+ value={appRole ?? UNASSIGNED}
762
+ onOpenChange={(open) => {
763
+ if (!open) setEditing(false);
764
+ }}
765
+ onValueChange={(value) => {
766
+ const next = value === UNASSIGNED ? null : value;
767
+ if (next === appRole) {
768
+ setEditing(false);
769
+ return;
770
+ }
771
+ setAppRole.mutate(
772
+ { email, role: next },
773
+ { onSuccess: () => setEditing(false) },
774
+ );
775
+ }}
776
+ disabled={setAppRole.isPending}
777
+ >
778
+ <SelectTrigger
779
+ autoFocus
780
+ className="h-auto w-auto rounded-md border border-border bg-background px-1.5 py-0.5 text-[11px]"
781
+ >
782
+ <SelectValue />
783
+ </SelectTrigger>
784
+ <SelectContent>
785
+ <SelectItem value={UNASSIGNED}>
786
+ {appRoles.defaultRole ? labelFor(appRoles.defaultRole) : "—"}
787
+ </SelectItem>
788
+ {appRoles.roles.map((r) => (
789
+ <SelectItem key={r} value={r}>
790
+ {labelFor(r)}
791
+ </SelectItem>
792
+ ))}
793
+ </SelectContent>
794
+ </Select>
795
+ </TableCell>
796
+ );
797
+ }
798
+
799
+ // An unassigned member shows the app's default only as a hint. The default
800
+ // never satisfies a server guard, so it must not read as a granted role.
801
+ const display = appRole ? (
802
+ <span className="inline-flex items-center rounded border border-border px-2 py-1 text-xs text-muted-foreground">
803
+ {labelFor(appRole)}
804
+ </span>
805
+ ) : (
806
+ <span className="text-xs text-muted-foreground/70">
807
+ {appRoles.defaultRole ? labelFor(appRoles.defaultRole) : "—"}
808
+ </span>
809
+ );
810
+
811
+ return (
812
+ <TableCell>
813
+ {canManage ? (
814
+ <Button
815
+ type="button"
816
+ onClick={() => setEditing(true)}
817
+ className="cursor-pointer rounded hover:opacity-80"
818
+ >
819
+ {display}
820
+ </Button>
821
+ ) : (
822
+ display
823
+ )}
824
+ </TableCell>
825
+ );
826
+ }
827
+
688
828
  function MemberRow({
689
829
  email,
690
830
  role,
691
831
  isCurrentUser,
692
832
  currentUserRole,
833
+ appRoles,
834
+ appRole,
835
+ canManageAppRoles,
693
836
  }: {
694
837
  email: string;
695
838
  role: string;
696
839
  isCurrentUser: boolean;
697
840
  currentUserRole: string | null;
841
+ appRoles?: AppRolesDescriptor;
842
+ appRole?: string | null;
843
+ canManageAppRoles?: boolean;
698
844
  }) {
699
845
  const t = useT();
700
846
  const removeMember = useRemoveMember();
@@ -719,6 +865,14 @@ function MemberRow({
719
865
  <TableCell>
720
866
  <RoleBadge role={role} />
721
867
  </TableCell>
868
+ {appRoles && (
869
+ <AppRoleCell
870
+ email={email}
871
+ appRoles={appRoles}
872
+ appRole={appRole ?? null}
873
+ canManage={Boolean(canManageAppRoles)}
874
+ />
875
+ )}
722
876
  <TableCell>
723
877
  {isCurrentUser ? (
724
878
  <span className="rounded border border-border px-1.5 py-0.5 text-[10px] text-muted-foreground">
@@ -1620,6 +1774,7 @@ export function TeamPage({
1620
1774
  showTitle = true,
1621
1775
  createOrgDescription,
1622
1776
  className,
1777
+ appRoles,
1623
1778
  }: TeamPageProps) {
1624
1779
  const t = useT();
1625
1780
  const { data: org, isLoading } = useOrg();
@@ -1652,7 +1807,7 @@ export function TeamPage({
1652
1807
  {!org?.orgId ? (
1653
1808
  <CreateOrgCard description={createOrgDescription} />
1654
1809
  ) : (
1655
- <MembersCard />
1810
+ <MembersCard appRoles={appRoles} />
1656
1811
  )}
1657
1812
  </>
1658
1813
  )}
@@ -318,6 +318,73 @@ export function useSetOrgDomain() {
318
318
  });
319
319
  }
320
320
 
321
+ export interface AppRoleAssignment {
322
+ email: string;
323
+ role: string;
324
+ }
325
+
326
+ export interface AppRolesInfo {
327
+ appId: string;
328
+ roles: string[];
329
+ /** Shown for members with no assignment. Never satisfies a server guard. */
330
+ defaultRole: string | null;
331
+ roleLabels: Record<string, string>;
332
+ /** Whether the current user may change assignments (org owner/admin). */
333
+ canManage: boolean;
334
+ assignments: AppRoleAssignment[];
335
+ myRole: string | null;
336
+ }
337
+
338
+ /**
339
+ * App-role vocabulary and assignments for the active org.
340
+ *
341
+ * `myRole` and `canManage` are progressive disclosure only — every guarded
342
+ * operation re-resolves both server-side, so a client that shows the wrong
343
+ * affordance still cannot perform the operation.
344
+ */
345
+ export function useAppRoles(appId: string | undefined) {
346
+ const { data: org } = useOrg();
347
+ return useQuery<AppRolesInfo>({
348
+ queryKey: ["org-app-roles", appId ?? null, org?.orgId ?? null],
349
+ queryFn: () =>
350
+ apiFetch(`${ORG_BASE}/app-roles?appId=${encodeURIComponent(appId!)}`),
351
+ enabled: Boolean(appId),
352
+ staleTime: 30_000,
353
+ });
354
+ }
355
+
356
+ /** The current user's role in one app, or `null` when unassigned. */
357
+ export function useAppRole(appId: string | undefined): {
358
+ role: string | null;
359
+ isLoading: boolean;
360
+ error: Error | null;
361
+ } {
362
+ const query = useAppRoles(appId);
363
+ return {
364
+ role: query.data?.myRole ?? null,
365
+ isLoading: query.isLoading,
366
+ error: query.error,
367
+ };
368
+ }
369
+
370
+ export function useSetAppMemberRole(appId: string) {
371
+ const qc = useQueryClient();
372
+ return useMutation<
373
+ { appId: string; email: string; role: string | null },
374
+ Error,
375
+ { email: string; role: string | null }
376
+ >({
377
+ mutationFn: ({ email, role }) =>
378
+ apiFetch(`${ORG_BASE}/app-roles/${encodeURIComponent(email)}`, {
379
+ method: "PUT",
380
+ body: JSON.stringify({ appId, role }),
381
+ }),
382
+ onSuccess: async () => {
383
+ await qc.invalidateQueries({ queryKey: ["org-app-roles"] });
384
+ },
385
+ });
386
+ }
387
+
321
388
  /**
322
389
  * Fetch the org's A2A secret on demand (owner/admin). Deliberately a separate
323
390
  * request from `useOrg()` so the secret only reaches the browser when the
@@ -19,6 +19,9 @@ export {
19
19
  useSetA2ASecret,
20
20
  useSyncA2ASecret,
21
21
  useOrgRole,
22
+ useAppRoles,
23
+ useAppRole,
24
+ useSetAppMemberRole,
22
25
  } from "./hooks.js";
23
26
 
24
27
  export type {
@@ -27,8 +30,14 @@ export type {
27
30
  BulkInviteResult,
28
31
  SyncA2ASecretResult,
29
32
  UseOrgRoleResult,
33
+ AppRoleAssignment,
34
+ AppRolesInfo,
30
35
  } from "./hooks.js";
31
36
 
37
+ // Type-only re-export so templates can annotate the `appRoles` prop without
38
+ // importing the server module.
39
+ export type { AppRolesDescriptor } from "../../org/app-roles.js";
40
+
32
41
  export { OrgSwitcher, type OrgSwitcherProps } from "./OrgSwitcher.js";
33
42
  export {
34
43
  InvitationBanner,
@@ -15,6 +15,8 @@ import { useCallback, useEffect, useRef, useState } from "react";
15
15
 
16
16
  const NETWORK_RESTART_BASE_MS = 1_000;
17
17
  const NETWORK_RESTART_MAX_MS = 30_000;
18
+ const RESTART_RETRY_BASE_MS = 400;
19
+ const MAX_RESTART_ATTEMPTS = 8;
18
20
 
19
21
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
20
22
  function getSpeechRecognitionCtor(): any {
@@ -42,6 +44,14 @@ export interface LiveTranscriptionApi {
42
44
  stopAndWait: (timeoutMs?: number) => Promise<string>;
43
45
  pause: () => void;
44
46
  resume: () => void;
47
+ /**
48
+ * Non-null when recognition died before the caller asked it to stop, so the
49
+ * returned transcript covers only part of the session. Callers must pass it
50
+ * along rather than storing a partial transcript as a finished one. Read
51
+ * through a getter (not state) so a stop handler can never observe a stale
52
+ * "everything was fine" value from an earlier render.
53
+ */
54
+ getIncompleteReason: () => string | null;
45
55
  }
46
56
 
47
57
  export function useLiveTranscription(
@@ -64,6 +74,20 @@ export function useLiveTranscription(
64
74
  const restartTimerRef = useRef<number | null>(null);
65
75
  const networkErrorCountRef = useRef(0);
66
76
  const restartDelayRef = useRef(0);
77
+ const restartFailureCountRef = useRef(0);
78
+ const incompleteReasonRef = useRef<string | null>(null);
79
+
80
+ // First reason wins: it names what actually broke the capture, and later
81
+ // teardown noise must not overwrite it.
82
+ const markIncomplete = useCallback((reason: string) => {
83
+ if (incompleteReasonRef.current) return;
84
+ incompleteReasonRef.current = reason;
85
+ }, []);
86
+
87
+ const getIncompleteReason = useCallback(
88
+ () => incompleteReasonRef.current,
89
+ [],
90
+ );
67
91
 
68
92
  const currentTranscript = useCallback(
69
93
  () =>
@@ -102,6 +126,8 @@ export function useLiveTranscription(
102
126
  stoppedManuallyRef.current = false;
103
127
  networkErrorCountRef.current = 0;
104
128
  restartDelayRef.current = 0;
129
+ restartFailureCountRef.current = 0;
130
+ incompleteReasonRef.current = null;
105
131
  setTranscript("");
106
132
  setInterimText("");
107
133
 
@@ -122,6 +148,7 @@ export function useLiveTranscription(
122
148
  if (event.results.length > 0) {
123
149
  networkErrorCountRef.current = 0;
124
150
  restartDelayRef.current = 0;
151
+ restartFailureCountRef.current = 0;
125
152
  }
126
153
  };
127
154
 
@@ -133,6 +160,9 @@ export function useLiveTranscription(
133
160
  event.error === "audio-capture"
134
161
  ) {
135
162
  stoppedManuallyRef.current = true;
163
+ markIncomplete(
164
+ `Browser speech recognition stopped mid-recording (${event.error}).`,
165
+ );
136
166
  setIsActive(false);
137
167
  return;
138
168
  }
@@ -163,17 +193,34 @@ export function useLiveTranscription(
163
193
  }
164
194
  try {
165
195
  recognition.start();
196
+ restartFailureCountRef.current = 0;
166
197
  } catch {
167
- setIsActive(false);
198
+ // Chrome throws InvalidStateError while the previous session is
199
+ // still releasing. `onend` has already fired and will not fire
200
+ // again, so this timer is the only thing left that can revive
201
+ // recognition — giving up here freezes the transcript mid-recording
202
+ // and the partial text then looks like the whole recording.
203
+ const attempt = ++restartFailureCountRef.current;
204
+ if (attempt >= MAX_RESTART_ATTEMPTS) {
205
+ markIncomplete(
206
+ "Browser speech recognition stopped mid-recording and could not be restarted.",
207
+ );
208
+ setIsActive(false);
209
+ return;
210
+ }
211
+ restartTimerRef.current = window.setTimeout(
212
+ restart,
213
+ RESTART_RETRY_BASE_MS * attempt,
214
+ );
168
215
  }
169
216
  };
170
- const delayMs = restartDelayRef.current;
217
+ // Never call start() synchronously from inside onend — Chrome still
218
+ // has the session marked as running during dispatch and throws.
219
+ restartTimerRef.current = window.setTimeout(
220
+ restart,
221
+ restartDelayRef.current,
222
+ );
171
223
  restartDelayRef.current = 0;
172
- if (delayMs > 0) {
173
- restartTimerRef.current = window.setTimeout(restart, delayMs);
174
- } else {
175
- restart();
176
- }
177
224
  return;
178
225
  }
179
226
  setIsActive(false);
@@ -185,10 +232,14 @@ export function useLiveTranscription(
185
232
  try {
186
233
  recognition.start();
187
234
  setIsActive(true);
188
- } catch {
189
- /* browser may block without user gesture */
235
+ } catch (err) {
236
+ // Browser may block without user gesture. Nothing was captured, and the
237
+ // caller must not treat the resulting empty text as "no speech".
238
+ markIncomplete(
239
+ `Browser speech recognition could not start (${(err as Error)?.message || "unknown error"}).`,
240
+ );
190
241
  }
191
- }, [currentTranscript, lang]);
242
+ }, [currentTranscript, lang, markIncomplete]);
192
243
 
193
244
  const stop = useCallback((): string => {
194
245
  const text = currentTranscript();
@@ -307,5 +358,6 @@ export function useLiveTranscription(
307
358
  stopAndWait,
308
359
  pause,
309
360
  resume,
361
+ getIncompleteReason,
310
362
  };
311
363
  }