@agent-native/dispatch 0.23.5 → 0.24.2

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 (224) hide show
  1. package/dist/actions/list-connected-agents.d.ts +1 -0
  2. package/dist/actions/list-connected-agents.js +5 -1
  3. package/dist/actions/list-connected-agents.js.map +1 -1
  4. package/dist/actions/list-dispatch-usage-metrics.d.ts +2 -0
  5. package/dist/actions/list-dispatch-usage-metrics.js +11 -1
  6. package/dist/actions/list-dispatch-usage-metrics.js.map +1 -1
  7. package/dist/actions/provider-api-register.d.ts +12 -12
  8. package/dist/actions/remix-workspace-template.js +4 -3
  9. package/dist/actions/remix-workspace-template.js.map +1 -1
  10. package/dist/actions/scaffold-workspace-app.js +3 -2
  11. package/dist/actions/scaffold-workspace-app.js.map +1 -1
  12. package/dist/actions/start-workspace-app-creation.js +3 -2
  13. package/dist/actions/start-workspace-app-creation.js.map +1 -1
  14. package/dist/actions/view-screen.js +11 -1
  15. package/dist/actions/view-screen.js.map +1 -1
  16. package/dist/components/admin-navigation.d.ts.map +1 -1
  17. package/dist/components/admin-navigation.js +21 -5
  18. package/dist/components/admin-navigation.js.map +1 -1
  19. package/dist/components/app-icon.js +1 -1
  20. package/dist/components/app-icon.js.map +1 -1
  21. package/dist/components/app-list-row.d.ts +1 -1
  22. package/dist/components/app-list-row.d.ts.map +1 -1
  23. package/dist/components/app-list-row.js +1 -1
  24. package/dist/components/app-list-row.js.map +1 -1
  25. package/dist/components/app-open-actions.d.ts +1 -17
  26. package/dist/components/app-open-actions.d.ts.map +1 -1
  27. package/dist/components/app-open-actions.js +1 -20
  28. package/dist/components/app-open-actions.js.map +1 -1
  29. package/dist/components/app-row-settings.d.ts +6 -0
  30. package/dist/components/app-row-settings.d.ts.map +1 -0
  31. package/dist/components/app-row-settings.js +11 -0
  32. package/dist/components/app-row-settings.js.map +1 -0
  33. package/dist/components/automation-run-history-dialog.d.ts.map +1 -1
  34. package/dist/components/automation-run-history-dialog.js +3 -2
  35. package/dist/components/automation-run-history-dialog.js.map +1 -1
  36. package/dist/components/connected-app-card.d.ts.map +1 -1
  37. package/dist/components/connected-app-card.js +3 -2
  38. package/dist/components/connected-app-card.js.map +1 -1
  39. package/dist/components/create-app-popover.js +2 -2
  40. package/dist/components/create-app-popover.js.map +1 -1
  41. package/dist/components/dispatch-control-plane.d.ts.map +1 -1
  42. package/dist/components/dispatch-control-plane.js +23 -7
  43. package/dist/components/dispatch-control-plane.js.map +1 -1
  44. package/dist/components/dispatch-shell.d.ts.map +1 -1
  45. package/dist/components/dispatch-shell.js +5 -1
  46. package/dist/components/dispatch-shell.js.map +1 -1
  47. package/dist/components/docs-link.d.ts +16 -0
  48. package/dist/components/docs-link.d.ts.map +1 -0
  49. package/dist/components/docs-link.js +34 -0
  50. package/dist/components/docs-link.js.map +1 -0
  51. package/dist/components/layout/Layout.d.ts +1 -0
  52. package/dist/components/layout/Layout.d.ts.map +1 -1
  53. package/dist/components/layout/Layout.js +43 -35
  54. package/dist/components/layout/Layout.js.map +1 -1
  55. package/dist/components/messaging-setup-panel.d.ts.map +1 -1
  56. package/dist/components/messaging-setup-panel.js +3 -2
  57. package/dist/components/messaging-setup-panel.js.map +1 -1
  58. package/dist/components/usage-alerts-panel.d.ts +11 -0
  59. package/dist/components/usage-alerts-panel.d.ts.map +1 -0
  60. package/dist/components/usage-alerts-panel.js +125 -0
  61. package/dist/components/usage-alerts-panel.js.map +1 -0
  62. package/dist/components/workspace-app-card.d.ts +3 -1
  63. package/dist/components/workspace-app-card.d.ts.map +1 -1
  64. package/dist/components/workspace-app-card.js +70 -29
  65. package/dist/components/workspace-app-card.js.map +1 -1
  66. package/dist/components/workspace-app-search.d.ts +10 -0
  67. package/dist/components/workspace-app-search.d.ts.map +1 -0
  68. package/dist/components/workspace-app-search.js +27 -0
  69. package/dist/components/workspace-app-search.js.map +1 -0
  70. package/dist/components/workspace-resource-effective-stack.d.ts.map +1 -1
  71. package/dist/components/workspace-resource-effective-stack.js +3 -2
  72. package/dist/components/workspace-resource-effective-stack.js.map +1 -1
  73. package/dist/components/workspace-template-card.d.ts.map +1 -1
  74. package/dist/components/workspace-template-card.js +10 -15
  75. package/dist/components/workspace-template-card.js.map +1 -1
  76. package/dist/hooks/use-navigation-state.d.ts +2 -0
  77. package/dist/hooks/use-navigation-state.d.ts.map +1 -1
  78. package/dist/hooks/use-navigation-state.js +10 -0
  79. package/dist/hooks/use-navigation-state.js.map +1 -1
  80. package/dist/lib/embed-session-recovery.d.ts +3 -0
  81. package/dist/lib/embed-session-recovery.d.ts.map +1 -0
  82. package/dist/lib/embed-session-recovery.js +18 -0
  83. package/dist/lib/embed-session-recovery.js.map +1 -0
  84. package/dist/lib/other-apps.d.ts +2 -0
  85. package/dist/lib/other-apps.d.ts.map +1 -1
  86. package/dist/lib/other-apps.js.map +1 -1
  87. package/dist/lib/workspace-app-layout.d.ts +19 -0
  88. package/dist/lib/workspace-app-layout.d.ts.map +1 -0
  89. package/dist/lib/workspace-app-layout.js +75 -0
  90. package/dist/lib/workspace-app-layout.js.map +1 -0
  91. package/dist/lib/workspace-apps.d.ts +14 -0
  92. package/dist/lib/workspace-apps.d.ts.map +1 -1
  93. package/dist/lib/workspace-apps.js +66 -0
  94. package/dist/lib/workspace-apps.js.map +1 -1
  95. package/dist/lib/workspace-apps.test.d.ts +2 -0
  96. package/dist/lib/workspace-apps.test.d.ts.map +1 -0
  97. package/dist/lib/workspace-apps.test.js +33 -0
  98. package/dist/lib/workspace-apps.test.js.map +1 -0
  99. package/dist/routes/pages/$appId.d.ts.map +1 -1
  100. package/dist/routes/pages/$appId.js +4 -2
  101. package/dist/routes/pages/$appId.js.map +1 -1
  102. package/dist/routes/pages/agents.d.ts.map +1 -1
  103. package/dist/routes/pages/agents.js +2 -3
  104. package/dist/routes/pages/agents.js.map +1 -1
  105. package/dist/routes/pages/apps.$appId.d.ts.map +1 -1
  106. package/dist/routes/pages/apps.$appId.js +17 -13
  107. package/dist/routes/pages/apps.$appId.js.map +1 -1
  108. package/dist/routes/pages/apps.d.ts.map +1 -1
  109. package/dist/routes/pages/apps.js +31 -16
  110. package/dist/routes/pages/apps.js.map +1 -1
  111. package/dist/routes/pages/audit.d.ts.map +1 -1
  112. package/dist/routes/pages/audit.js +2 -1
  113. package/dist/routes/pages/audit.js.map +1 -1
  114. package/dist/routes/pages/chat.d.ts.map +1 -1
  115. package/dist/routes/pages/chat.js +7 -2
  116. package/dist/routes/pages/chat.js.map +1 -1
  117. package/dist/routes/pages/dreams.d.ts.map +1 -1
  118. package/dist/routes/pages/dreams.js +14 -18
  119. package/dist/routes/pages/dreams.js.map +1 -1
  120. package/dist/routes/pages/integrations.d.ts.map +1 -1
  121. package/dist/routes/pages/integrations.js +51 -14
  122. package/dist/routes/pages/integrations.js.map +1 -1
  123. package/dist/routes/pages/metrics.d.ts.map +1 -1
  124. package/dist/routes/pages/metrics.js +146 -17
  125. package/dist/routes/pages/metrics.js.map +1 -1
  126. package/dist/routes/pages/operations.js +1 -1
  127. package/dist/routes/pages/operations.js.map +1 -1
  128. package/dist/routes/pages/settings.d.ts.map +1 -1
  129. package/dist/routes/pages/settings.js +19 -2
  130. package/dist/routes/pages/settings.js.map +1 -1
  131. package/dist/routes/pages/thread-debug.d.ts.map +1 -1
  132. package/dist/routes/pages/thread-debug.js +5 -8
  133. package/dist/routes/pages/thread-debug.js.map +1 -1
  134. package/dist/routes/pages/vault.d.ts.map +1 -1
  135. package/dist/routes/pages/vault.js +1 -1
  136. package/dist/routes/pages/vault.js.map +1 -1
  137. package/dist/routes/pages/workspace.d.ts.map +1 -1
  138. package/dist/routes/pages/workspace.js +13 -10
  139. package/dist/routes/pages/workspace.js.map +1 -1
  140. package/dist/server/index.d.ts.map +1 -1
  141. package/dist/server/index.js +3 -0
  142. package/dist/server/index.js.map +1 -1
  143. package/dist/server/lib/app-creation-store.d.ts.map +1 -1
  144. package/dist/server/lib/app-creation-store.js +12 -52
  145. package/dist/server/lib/app-creation-store.js.map +1 -1
  146. package/dist/server/lib/usage-metrics-store.d.ts +17 -0
  147. package/dist/server/lib/usage-metrics-store.d.ts.map +1 -1
  148. package/dist/server/lib/usage-metrics-store.js +187 -30
  149. package/dist/server/lib/usage-metrics-store.js.map +1 -1
  150. package/dist/server/plugins/agent-chat.js +2 -0
  151. package/dist/server/plugins/agent-chat.js.map +1 -1
  152. package/dist/server/plugins/integrations.d.ts.map +1 -1
  153. package/dist/server/plugins/integrations.js +1 -0
  154. package/dist/server/plugins/integrations.js.map +1 -1
  155. package/dist/shared/app-roles.d.ts +9 -0
  156. package/dist/shared/app-roles.d.ts.map +1 -0
  157. package/dist/shared/app-roles.js +7 -0
  158. package/dist/shared/app-roles.js.map +1 -0
  159. package/package.json +4 -3
  160. package/src/actions/list-connected-agents.spec.ts +73 -0
  161. package/src/actions/list-connected-agents.ts +6 -2
  162. package/src/actions/list-dispatch-usage-metrics.ts +11 -1
  163. package/src/actions/remix-workspace-template.spec.ts +29 -0
  164. package/src/actions/remix-workspace-template.ts +13 -6
  165. package/src/actions/scaffold-workspace-app.ts +12 -5
  166. package/src/actions/start-workspace-app-creation.ts +8 -2
  167. package/src/actions/view-screen.ts +13 -1
  168. package/src/components/admin-navigation.tsx +57 -11
  169. package/src/components/app-icon.spec.tsx +34 -0
  170. package/src/components/app-icon.tsx +1 -1
  171. package/src/components/app-list-row.tsx +1 -2
  172. package/src/components/app-open-actions.tsx +5 -114
  173. package/src/components/app-row-settings.tsx +54 -0
  174. package/src/components/automation-run-history-dialog.tsx +8 -4
  175. package/src/components/connected-app-card.tsx +3 -2
  176. package/src/components/create-app-popover.spec.tsx +1 -1
  177. package/src/components/create-app-popover.tsx +2 -2
  178. package/src/components/dispatch-control-plane.spec.tsx +44 -3
  179. package/src/components/dispatch-control-plane.tsx +83 -27
  180. package/src/components/dispatch-shell.tsx +7 -1
  181. package/src/components/docs-link.tsx +52 -0
  182. package/src/components/layout/Layout.spec.tsx +13 -0
  183. package/src/components/layout/Layout.tsx +63 -32
  184. package/src/components/messaging-setup-panel.tsx +10 -3
  185. package/src/components/usage-alerts-panel.tsx +445 -0
  186. package/src/components/workspace-app-card.spec.tsx +130 -9
  187. package/src/components/workspace-app-card.tsx +128 -71
  188. package/src/components/workspace-app-search.tsx +96 -0
  189. package/src/components/workspace-resource-effective-stack.tsx +10 -5
  190. package/src/components/workspace-template-card.spec.tsx +53 -24
  191. package/src/components/workspace-template-card.tsx +16 -26
  192. package/src/hooks/use-navigation-state.ts +12 -0
  193. package/src/lib/embed-session-recovery.spec.ts +63 -0
  194. package/src/lib/embed-session-recovery.ts +26 -0
  195. package/src/lib/other-apps.ts +2 -0
  196. package/src/lib/workspace-app-layout.spec.ts +47 -0
  197. package/src/lib/workspace-app-layout.ts +121 -0
  198. package/src/lib/workspace-apps.test.ts +64 -0
  199. package/src/lib/workspace-apps.ts +82 -0
  200. package/src/routes/pages/$appId.tsx +3 -6
  201. package/src/routes/pages/agents.tsx +6 -3
  202. package/src/routes/pages/apps.$appId.tsx +16 -17
  203. package/src/routes/pages/apps.tsx +116 -62
  204. package/src/routes/pages/audit.tsx +28 -8
  205. package/src/routes/pages/chat.spec.tsx +10 -0
  206. package/src/routes/pages/chat.tsx +8 -0
  207. package/src/routes/pages/dreams.tsx +92 -142
  208. package/src/routes/pages/integrations.tsx +147 -68
  209. package/src/routes/pages/metrics.tsx +460 -102
  210. package/src/routes/pages/operations.tsx +2 -2
  211. package/src/routes/pages/settings.tsx +27 -10
  212. package/src/routes/pages/thread-debug.tsx +18 -36
  213. package/src/routes/pages/vault.tsx +7 -2
  214. package/src/routes/pages/workspace.tsx +92 -56
  215. package/src/server/index.ts +4 -0
  216. package/src/server/lib/app-creation-store.spec.ts +90 -43
  217. package/src/server/lib/app-creation-store.ts +15 -61
  218. package/src/server/lib/usage-metrics-store.spec.ts +117 -0
  219. package/src/server/lib/usage-metrics-store.ts +249 -30
  220. package/src/server/plugins/agent-chat.spec.ts +4 -0
  221. package/src/server/plugins/agent-chat.ts +2 -0
  222. package/src/server/plugins/integrations.ts +1 -0
  223. package/src/shared/app-roles.ts +8 -0
  224. package/src/styles/dispatch.css +9 -0
@@ -1 +1 @@
1
- {"version":3,"file":"integrations.js","sourceRoot":"","sources":["../../../src/server/plugins/integrations.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC;AAErE,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EACL,qBAAqB,EACrB,+BAA+B,GAChC,MAAM,iCAAiC,CAAC;AAEzC,MAAM,0BAA0B,GAAG;IACjC,GAAG,eAAe;IAClB,2EAA2E;IAC3E,6EAA6E;IAC7E,6EAA6E;IAC7E,OAAO,EAAE;QACP,GAAG,eAAe,CAAC,OAAO;QAC1B,SAAS,EAAE,KAAK;KACjB;CACF,CAAC;AAEF,MAAM,kCAAkC,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;4FA4BiD,CAAC;AAE7F;;;;GAIG;AACH,MAAM,0BAA0B,GAAG,KAAK,EAAE,QAAa,EAAE,EAAE;IACzD,MAAM,EAAE,YAAY,GAAG,EAAE,EAAE,GAAG,iBAAiB,EAAE,CAAC;IAClD,MAAM,cAAc,GAAG,YAAY,CAAC,YAAY,CAAC;IACjD,MAAM,YAAY,GAChB,OAAO,cAAc,KAAK,QAAQ;QAChC,CAAC,CAAC,cAAc;QAChB,CAAC,CAAC,OAAO,cAAc,KAAK,UAAU;YACpC,CAAC,CAAC,cAAc,CAAC,kCAAkC,CAAC;YACpD,CAAC,CAAC,kCAAkC,CAAC;IAE3C,MAAM,MAAM,GAAG,wBAAwB,CAAC;QACtC,KAAK,EAAE,UAAU;QACjB,OAAO,EAAE,0BAA0B;QACnC,uBAAuB,EAAE,+BAA+B;QACxD,aAAa,EAAE,qBAAqB;QACpC,YAAY;QACZ,wDAAwD;QACxD,yEAAyE;QACzE,+DAA+D;QAC/D,6EAA6E;KAC9E,CAAC,CAAC;IAEH,OAAO,MAAM,CAAC,QAAQ,CAAC,CAAC;AAC1B,CAAC,CAAC;AAEF,eAAe,0BAA0B,CAAC","sourcesContent":["import { createIntegrationsPlugin } from \"@agent-native/core/server\";\n\nimport { dispatchActions } from \"../../actions/index.js\";\nimport { getDispatchConfig } from \"../index.js\";\nimport {\n beforeDispatchProcess,\n resolveDispatchExecutionContext,\n} from \"../lib/dispatch-integrations.js\";\n\nconst dispatchIntegrationActions = {\n ...dispatchActions,\n // Messaging integrations should use the core call-agent tool for cross-app\n // delegation because it queues A2A continuations when serverless budgets are\n // tight. The MCP-facing ask_app action is still available outside this path.\n ask_app: {\n ...dispatchActions.ask_app,\n agentTool: false,\n },\n};\n\nconst DISPATCH_INTEGRATION_SYSTEM_PROMPT = `You are the central dispatch for this workspace, responding via a messaging platform integration (Slack, Telegram, email, etc.).\n\nDefault posture:\n- Treat Slack, Telegram, and email as shared entrypoints into the workspace.\n- Heavily delegate domain work to specialized agents through A2A (call-agent) when another app owns the job. Apps you can delegate to include slides (decks/presentations), analytics (data/dashboards), content (docs/articles), forms (form builder), clips (screen recordings), design (visual designs), and assets (brand libraries plus generated images/videos).\n- Use the available-apps prompt context first, then list-connected-agents when you need fresh details, to see what agents are available before assuming a request must be handled locally.\n- Hosted/connected A2A neighbors such as Analytics and Content come from the available-apps context or list-connected-agents. list-workspace-apps only inventories apps mounted inside this workspace deployment; never use a missing row there to conclude that a connected agent is unavailable.\n- When asked whether a mounted workspace app exposes an agent card or A2A endpoint, call list-workspace-apps with includeAgentCards=true. Without that probe, missing agent-card fields mean unchecked, not unavailable.\n- Treat first-party apps such as Mail, Calendar, Analytics, Brain, Assets, and Dispatch as existing hosted/connected neighbors available through links and A2A/default connected agents. Do not create wrapper apps, child apps, nested routes, or cloned template copies just to give a new app access to them; build only the genuinely new workflow and delegate cross-app work to those existing apps.\n- Integration grants are not provider capability limits. For ad hoc provider inspection, querying, reporting, or troubleshooting, call provider-api-catalog/provider-api-docs, then provider-api-request against the provider's real HTTP API. Use connectionId for a specific shared grant and accountId for a specific OAuth account. Never expose secret values or silently widen app access while doing this.\n- Keep durable memory and operating instructions in resources rather than ephemeral chat.\n- Reply in the originating thread unless the user explicitly asks you to send to a saved destination.\n\nWhen a user asks for something:\n- If it belongs to analytics, content, slides, clips, assets, etc., delegate via call-agent — do not re-implement the domain logic in dispatch.\n- Synthetic uptime, health-check, and URL-availability monitors belong to Analytics, even when the monitored target is another app such as Clips. Delegate creation to Analytics and relay its exact monitor URL. Dispatch-native recurring jobs are for reminders, digests, and agent workflows, not HTTP uptime probes.\n- Route by the requested artifact type, not by organization-specific names stored in code. For structured records, databases, tables, queues, boards, and intake forms, resolve the owning app and canonical destination from loaded workspace instructions/resources plus discovered app capabilities; do not assume Content, a database ID, schema, owner, or required fields. Visual designs, mockups, wireframes, screens, and interfaces belong to Design. A trusted Required target agent hint in integration context is authoritative.\n- When delegating structured intake to the resolved owning app, preserve the exact Source thread URL and the workspace instruction context, inspect the destination's current required fields, ask only for missing values, submit once, verify the saved record, and return the exact link.\n- In messaging integrations, use call-agent for cross-app delegation; do not use ask_app.\n- After call-agent returns an answer, RELAY IT DIRECTLY to the user with at most a one-line preface — do not rephrase, summarize, or add commentary. The downstream agent already crafted the answer; your job is delivery, not editing. This minimizes round-trips and keeps the user-visible reply fast.\n- Exception: if the downstream agent reports a missing model/provider credential, do not name exact env vars, Vault keys, tokens, or secrets. Say the target app needs an LLM connection and recommend connecting Builder/managed LLM for that app; keep bring-your-own provider keys as a secondary option only if the user asks.\n- If the user asks to create, build, make, scaffold, or generate an \"agent\" from Dispatch chat or by tagging @agent-native in Slack, email, or Telegram, first classify the ask. If it is a simple Dispatch-native behavior like a reminder, digest, monitor, routing rule, saved instruction, or recurring workflow, create or update the recurring job/resource/destination in Dispatch. If it is a robust unique product or teammate that needs its own UI, data model, actions, integrations, or domain workflow, treat it as a new workspace app and call start-workspace-app-creation.\n- If a new-app prompt asks for access to Mail, Calendar, Analytics, Brain, Assets, or similar first-party app data/agents, keep using the existing hosted/connected app and A2A path. Do not ask Builder to scaffold those apps as children of the new app unless the user explicitly asks for a customized app from that template.\n- If the chat template is used, treat it as scaffolding only: the finished app must be branded as the requested app with its own home screen/navigation/package metadata/manifest, and must not leave visible \"Chat\", \"Starter\", \"Blank app\", or \"New app\" UI behind.\n- If the user explicitly asks for a new app or workspace app, call start-workspace-app-creation with their prompt and include a concise generated description by default. Do not satisfy a new-app request by adding a route, page, component, or file inside apps/chat or another existing app unless the user explicitly asks to modify that existing app. When the prompt contains a source brief or concrete repeatable workflow, choose recommended defaults and do not ask non-blocking product or UX questions; ask only for authorization, credentials, a destructive action, an ambiguous target workspace, or a genuinely missing workflow. If the request is too vague to classify, ask one concise follow-up. If the action returns mode \"builder\", reply with the Builder branch URL; Builder is responsible for creating the separate workspace app under apps/<app-id>, mounting it at /<app-id>, ensuring apps/<app-id>/package.json exists with name/displayName and description so Dispatch discovers it, using relative /<app-id> links instead of hardcoded localhost/dev ports, and preserving APP_BASE_PATH/VITE_APP_BASE_PATH in the React Router client entry. The new app lives at the workspace root /<app-id>, NOT under /dispatch/<app-id>, /apps/<app-id>, or any other Dispatch tab — when telling the user where to find it, link to /<app-id> only. There is no separate workspace app registry to edit. If it returns mode \"local-agent\", tell the user it is ready for the local code agent and include the returned app path/prompt summary. Dispatch reuses the connected workspace Builder project or provisions it through the Builder Projects API when an organization owner/admin starts the first app; members need an owner/admin to configure the project first. Do not ask the user to use Builder org/beta settings. If it returns mode \"builder-unavailable\", the action also returns a \\`reason\\` code plus a user-facing \\`message\\` and (for some reasons) an operator-facing \\`detail\\`. Relay the \\`message\\` to the user as-is; only surface \\`detail\\` when the user is clearly an operator debugging the deployment (e.g. troubleshooting a broken Builder connection for the workspace), never as routine detail for an end user.\n- For digests, reminders, or saved behavior, prefer recurring jobs, resources, or destinations over chat replies.\n- Keep responses concise and operational — messaging platforms have character limits.\n- Use markdown sparingly (bold and lists are fine, avoid complex formatting).\n- If a task requires many steps, summarize what you did rather than streaming every detail.`;\n\n/**\n * Defer plugin construction until the Nitro plugin actually fires so the\n * config-aware system prompt resolves AFTER `setupDispatch(config)` has\n * stamped the active config (plugin module load order is not guaranteed).\n */\nconst dispatchIntegrationsPlugin = async (nitroApp: any) => {\n const { integrations = {} } = getDispatchConfig();\n const promptOverride = integrations.systemPrompt;\n const systemPrompt =\n typeof promptOverride === \"string\"\n ? promptOverride\n : typeof promptOverride === \"function\"\n ? promptOverride(DISPATCH_INTEGRATION_SYSTEM_PROMPT)\n : DISPATCH_INTEGRATION_SYSTEM_PROMPT;\n\n const plugin = createIntegrationsPlugin({\n appId: \"dispatch\",\n actions: dispatchIntegrationActions,\n resolveExecutionContext: resolveDispatchExecutionContext,\n beforeProcess: beforeDispatchProcess,\n systemPrompt,\n // Inherit the framework default (claude-sonnet-4-6 from\n // packages/core/src/integrations/plugin.ts). Haiku was tried for latency\n // but hallucinated URLs/IDs after delegated call-agent results\n // (e.g. inventing `https://slides.workspace.com/deck/builder-io-deck-2024`).\n });\n\n return plugin(nitroApp);\n};\n\nexport default dispatchIntegrationsPlugin;\n"]}
1
+ {"version":3,"file":"integrations.js","sourceRoot":"","sources":["../../../src/server/plugins/integrations.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC;AAErE,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EACL,qBAAqB,EACrB,+BAA+B,GAChC,MAAM,iCAAiC,CAAC;AAEzC,MAAM,0BAA0B,GAAG;IACjC,GAAG,eAAe;IAClB,2EAA2E;IAC3E,6EAA6E;IAC7E,6EAA6E;IAC7E,OAAO,EAAE;QACP,GAAG,eAAe,CAAC,OAAO;QAC1B,SAAS,EAAE,KAAK;KACjB;CACF,CAAC;AAEF,MAAM,kCAAkC,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;4FA6BiD,CAAC;AAE7F;;;;GAIG;AACH,MAAM,0BAA0B,GAAG,KAAK,EAAE,QAAa,EAAE,EAAE;IACzD,MAAM,EAAE,YAAY,GAAG,EAAE,EAAE,GAAG,iBAAiB,EAAE,CAAC;IAClD,MAAM,cAAc,GAAG,YAAY,CAAC,YAAY,CAAC;IACjD,MAAM,YAAY,GAChB,OAAO,cAAc,KAAK,QAAQ;QAChC,CAAC,CAAC,cAAc;QAChB,CAAC,CAAC,OAAO,cAAc,KAAK,UAAU;YACpC,CAAC,CAAC,cAAc,CAAC,kCAAkC,CAAC;YACpD,CAAC,CAAC,kCAAkC,CAAC;IAE3C,MAAM,MAAM,GAAG,wBAAwB,CAAC;QACtC,KAAK,EAAE,UAAU;QACjB,OAAO,EAAE,0BAA0B;QACnC,uBAAuB,EAAE,+BAA+B;QACxD,aAAa,EAAE,qBAAqB;QACpC,YAAY;QACZ,wDAAwD;QACxD,yEAAyE;QACzE,+DAA+D;QAC/D,6EAA6E;KAC9E,CAAC,CAAC;IAEH,OAAO,MAAM,CAAC,QAAQ,CAAC,CAAC;AAC1B,CAAC,CAAC;AAEF,eAAe,0BAA0B,CAAC","sourcesContent":["import { createIntegrationsPlugin } from \"@agent-native/core/server\";\n\nimport { dispatchActions } from \"../../actions/index.js\";\nimport { getDispatchConfig } from \"../index.js\";\nimport {\n beforeDispatchProcess,\n resolveDispatchExecutionContext,\n} from \"../lib/dispatch-integrations.js\";\n\nconst dispatchIntegrationActions = {\n ...dispatchActions,\n // Messaging integrations should use the core call-agent tool for cross-app\n // delegation because it queues A2A continuations when serverless budgets are\n // tight. The MCP-facing ask_app action is still available outside this path.\n ask_app: {\n ...dispatchActions.ask_app,\n agentTool: false,\n },\n};\n\nconst DISPATCH_INTEGRATION_SYSTEM_PROMPT = `You are the central dispatch for this workspace, responding via a messaging platform integration (Slack, Telegram, email, etc.).\n\nDefault posture:\n- Treat Slack, Telegram, and email as shared entrypoints into the workspace.\n- Heavily delegate domain work to specialized agents through A2A (call-agent) when another app owns the job. Apps you can delegate to include slides (decks/presentations), analytics (data/dashboards), content (docs/articles), forms (form builder), clips (screen recordings), design (visual designs), and assets (brand libraries plus generated images/videos).\n- Use the available-apps prompt context first, then list-connected-agents when you need fresh details, to see what agents are available before assuming a request must be handled locally.\n- Hosted/connected A2A neighbors such as Analytics and Content come from the available-apps context or list-connected-agents. list-workspace-apps only inventories apps mounted inside this workspace deployment; never use a missing row there to conclude that a connected agent is unavailable.\n- When asked whether a mounted workspace app exposes an agent card or A2A endpoint, call list-workspace-apps with includeAgentCards=true. Without that probe, missing agent-card fields mean unchecked, not unavailable.\n- Treat first-party apps such as Mail, Calendar, Analytics, Brain, Assets, and Dispatch as existing hosted/connected neighbors available through links and A2A/default connected agents. Do not create wrapper apps, child apps, nested routes, or cloned template copies just to give a new app access to them; build only the genuinely new workflow and delegate cross-app work to those existing apps.\n- Integration grants are not provider capability limits. For ad hoc provider inspection, querying, reporting, or troubleshooting, call provider-api-catalog/provider-api-docs, then provider-api-request against the provider's real HTTP API. Use connectionId for a specific shared grant and accountId for a specific OAuth account. Never expose secret values or silently widen app access while doing this.\n- Keep durable memory and operating instructions in resources rather than ephemeral chat.\n- Reply in the originating thread unless the user explicitly asks you to send to a saved destination.\n\nWhen a user asks for something:\n- If it belongs to analytics, content, slides, clips, assets, etc., delegate via call-agent — do not re-implement the domain logic in dispatch.\n- Synthetic uptime, health-check, and URL-availability monitors belong to Analytics, even when the monitored target is another app such as Clips. Delegate creation to Analytics and relay its exact monitor URL. Dispatch-native recurring jobs are for reminders, digests, and agent workflows, not HTTP uptime probes.\n- Route by the requested artifact type, not by organization-specific names stored in code. For structured records, databases, tables, queues, boards, and intake forms, resolve the owning app and canonical destination from loaded workspace instructions/resources plus discovered app capabilities; do not assume Content, a database ID, schema, owner, or required fields. Visual designs, mockups, wireframes, screens, and interfaces belong to Design. A trusted Required target agent hint in integration context is authoritative.\n- When delegating structured intake to the resolved owning app, preserve the exact Source thread URL and the workspace instruction context, inspect the destination's current required fields, ask only for missing values, submit once, verify the saved record, and return the exact link.\n- In messaging integrations, use call-agent for cross-app delegation; do not use ask_app.\n- After call-agent returns an answer, RELAY IT DIRECTLY to the user with at most a one-line preface — do not rephrase, summarize, or add commentary. The downstream agent already crafted the answer; your job is delivery, not editing. This minimizes round-trips and keeps the user-visible reply fast.\n- Exception: if the downstream agent reports a missing model/provider credential, do not name exact env vars, Vault keys, tokens, or secrets. Say the target app needs an LLM connection and recommend connecting Builder/managed LLM for that app; keep bring-your-own provider keys as a secondary option only if the user asks.\n- If the user asks to create, build, make, scaffold, or generate an \"agent\" from Dispatch chat or by tagging @agent-native in Slack, email, or Telegram, first classify the ask. If it is a simple Dispatch-native behavior like a reminder, digest, monitor, routing rule, saved instruction, or recurring workflow, create or update the recurring job/resource/destination in Dispatch. If it is a robust unique product or teammate that needs its own UI, data model, actions, integrations, or domain workflow, treat it as a new workspace app and call start-workspace-app-creation.\n- If a user asks to rename an existing workspace app or change its Dispatch title/description, call update-workspace-app-metadata with the existing appId from list-workspace-apps. Do not call start-workspace-app-creation for a metadata-only edit because that starts a new app and Builder branch.\n- If a new-app prompt asks for access to Mail, Calendar, Analytics, Brain, Assets, or similar first-party app data/agents, keep using the existing hosted/connected app and A2A path. Do not ask Builder to scaffold those apps as children of the new app unless the user explicitly asks for a customized app from that template.\n- If the chat template is used, treat it as scaffolding only: the finished app must be branded as the requested app with its own home screen/navigation/package metadata/manifest, and must not leave visible \"Chat\", \"Starter\", \"Blank app\", or \"New app\" UI behind.\n- If the user explicitly asks for a new app or workspace app, call start-workspace-app-creation with their prompt and include a concise generated description by default. Do not satisfy a new-app request by adding a route, page, component, or file inside apps/chat or another existing app unless the user explicitly asks to modify that existing app. When the prompt contains a source brief or concrete repeatable workflow, choose recommended defaults and do not ask non-blocking product or UX questions; ask only for authorization, credentials, a destructive action, an ambiguous target workspace, or a genuinely missing workflow. If the request is too vague to classify, ask one concise follow-up. If the action returns mode \"builder\", reply with the Builder branch URL; Builder is responsible for creating the separate workspace app under apps/<app-id>, mounting it at /<app-id>, ensuring apps/<app-id>/package.json exists with name/displayName and description so Dispatch discovers it, using relative /<app-id> links instead of hardcoded localhost/dev ports, and preserving APP_BASE_PATH/VITE_APP_BASE_PATH in the React Router client entry. The new app lives at the workspace root /<app-id>, NOT under /dispatch/<app-id>, /apps/<app-id>, or any other Dispatch tab — when telling the user where to find it, link to /<app-id> only. There is no separate workspace app registry to edit. If it returns mode \"local-agent\", tell the user it is ready for the local code agent and include the returned app path/prompt summary. Dispatch reuses the connected workspace Builder project or provisions it through the Builder Projects API when an organization owner/admin starts the first app; members need an owner/admin to configure the project first. Do not ask the user to use Builder org/beta settings. If it returns mode \"builder-unavailable\", the action also returns a \\`reason\\` code plus a user-facing \\`message\\` and (for some reasons) an operator-facing \\`detail\\`. Relay the \\`message\\` to the user as-is; only surface \\`detail\\` when the user is clearly an operator debugging the deployment (e.g. troubleshooting a broken Builder connection for the workspace), never as routine detail for an end user.\n- For digests, reminders, or saved behavior, prefer recurring jobs, resources, or destinations over chat replies.\n- Keep responses concise and operational — messaging platforms have character limits.\n- Use markdown sparingly (bold and lists are fine, avoid complex formatting).\n- If a task requires many steps, summarize what you did rather than streaming every detail.`;\n\n/**\n * Defer plugin construction until the Nitro plugin actually fires so the\n * config-aware system prompt resolves AFTER `setupDispatch(config)` has\n * stamped the active config (plugin module load order is not guaranteed).\n */\nconst dispatchIntegrationsPlugin = async (nitroApp: any) => {\n const { integrations = {} } = getDispatchConfig();\n const promptOverride = integrations.systemPrompt;\n const systemPrompt =\n typeof promptOverride === \"string\"\n ? promptOverride\n : typeof promptOverride === \"function\"\n ? promptOverride(DISPATCH_INTEGRATION_SYSTEM_PROMPT)\n : DISPATCH_INTEGRATION_SYSTEM_PROMPT;\n\n const plugin = createIntegrationsPlugin({\n appId: \"dispatch\",\n actions: dispatchIntegrationActions,\n resolveExecutionContext: resolveDispatchExecutionContext,\n beforeProcess: beforeDispatchProcess,\n systemPrompt,\n // Inherit the framework default (claude-sonnet-4-6 from\n // packages/core/src/integrations/plugin.ts). Haiku was tried for latency\n // but hallucinated URLs/IDs after delegated call-agent results\n // (e.g. inventing `https://slides.workspace.com/deck/builder-io-deck-2024`).\n });\n\n return plugin(nitroApp);\n};\n\nexport default dispatchIntegrationsPlugin;\n"]}
@@ -0,0 +1,9 @@
1
+ export declare const dispatchAccessDescriptor: {
2
+ appId: string;
3
+ roles: readonly ["admin"];
4
+ roleLabels: {
5
+ admin: string;
6
+ };
7
+ label: string;
8
+ };
9
+ //# sourceMappingURL=app-roles.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app-roles.d.ts","sourceRoot":"","sources":["../../src/shared/app-roles.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,wBAAwB;;;;QAGrB,KAAK;;;CAEkB,CAAC"}
@@ -0,0 +1,7 @@
1
+ export const dispatchAccessDescriptor = {
2
+ appId: "dispatch",
3
+ roles: ["admin"],
4
+ roleLabels: { admin: "Dispatch admin" },
5
+ label: "Dispatch access",
6
+ };
7
+ //# sourceMappingURL=app-roles.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app-roles.js","sourceRoot":"","sources":["../../src/shared/app-roles.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,wBAAwB,GAAG;IACtC,KAAK,EAAE,UAAU;IACjB,KAAK,EAAE,CAAC,OAAO,CAAU;IACzB,UAAU,EAAE,EAAE,KAAK,EAAE,gBAAgB,EAAE;IACvC,KAAK,EAAE,iBAAiB;CACa,CAAC","sourcesContent":["import type { AppRolesDescriptor } from \"@agent-native/core/client/org\";\n\nexport const dispatchAccessDescriptor = {\n appId: \"dispatch\",\n roles: [\"admin\"] as const,\n roleLabels: { admin: \"Dispatch admin\" },\n label: \"Dispatch access\",\n} satisfies AppRolesDescriptor<\"admin\">;\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/dispatch",
3
- "version": "0.23.5",
3
+ "version": "0.24.2",
4
4
  "description": "Dispatch — workspace control plane for agent-native apps. Vault, integrations, destinations, scheduled jobs, and cross-app delegation, shipped as a single drop-in package.",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -28,6 +28,7 @@
28
28
  "./routes": "./dist/routes/index.js",
29
29
  "./routes/pages/*": "./dist/routes/pages/*.js",
30
30
  "./server": "./dist/server/index.js",
31
+ "./shared/app-roles": "./dist/shared/app-roles.js",
31
32
  "./server/lib/thread-link-preview": "./dist/server/lib/thread-link-preview.js",
32
33
  "./actions": "./dist/actions/index.js",
33
34
  "./db": "./dist/db/index.js",
@@ -84,7 +85,7 @@
84
85
  "tailwind-merge": "^3.5.0",
85
86
  "vaul": "^1.1.2",
86
87
  "zod": "^4.3.6",
87
- "@agent-native/toolkit": "0.13.10"
88
+ "@agent-native/toolkit": "0.14.1"
88
89
  },
89
90
  "devDependencies": {
90
91
  "@react-router/dev": "^8.1.0",
@@ -98,7 +99,7 @@
98
99
  "typescript-7": "npm:typescript@^7.0.2",
99
100
  "vite": "8.1.0",
100
101
  "vitest": "^4.1.5",
101
- "@agent-native/core": "0.151.0"
102
+ "@agent-native/core": "0.153.0"
102
103
  },
103
104
  "peerDependencies": {
104
105
  "@agent-native/core": ">=0.8.0",
@@ -0,0 +1,73 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+
3
+ const mocks = vi.hoisted(() => ({
4
+ discoverAgents: vi.fn(),
5
+ getBuiltinAgents: vi.fn(),
6
+ getRequestUserEmail: vi.fn(() => "owner@example.test"),
7
+ resourceGet: vi.fn(),
8
+ resourceListAccessible: vi.fn(async () => []),
9
+ }));
10
+
11
+ vi.mock("@agent-native/core", () => ({
12
+ defineAction: (options: unknown) => options,
13
+ }));
14
+
15
+ vi.mock("@agent-native/core/resources/metadata", () => ({
16
+ parseRemoteAgentManifest: vi.fn(),
17
+ REMOTE_AGENT_RESOURCE_PREFIXES: [],
18
+ }));
19
+
20
+ vi.mock("@agent-native/core/resources/store", () => ({
21
+ resourceGet: (...args: unknown[]) => mocks.resourceGet(...args),
22
+ resourceListAccessible: (...args: unknown[]) =>
23
+ mocks.resourceListAccessible(...args),
24
+ SHARED_OWNER: "shared",
25
+ }));
26
+
27
+ vi.mock("@agent-native/core/server", () => ({
28
+ getRequestUserEmail: () => mocks.getRequestUserEmail(),
29
+ }));
30
+
31
+ vi.mock("@agent-native/core/server/agent-discovery", () => ({
32
+ discoverAgents: (...args: unknown[]) => mocks.discoverAgents(...args),
33
+ getBuiltinAgents: (...args: unknown[]) => mocks.getBuiltinAgents(...args),
34
+ normalizeAgentId: (id: string) => id.trim().toLowerCase(),
35
+ shouldIncludeRemoteAgentManifest: vi.fn(),
36
+ }));
37
+
38
+ vi.mock("../server/index.js", () => ({
39
+ getDispatchConfig: vi.fn(async () => ({ hiddenAgentIds: [] })),
40
+ }));
41
+
42
+ describe("list-connected-agents", () => {
43
+ it("keeps a built-in app home URL separate from its A2A endpoint", async () => {
44
+ mocks.getBuiltinAgents.mockReturnValue([
45
+ {
46
+ id: "clips",
47
+ name: "Clips",
48
+ description: "Record and share",
49
+ url: "https://clips.agent-native.com",
50
+ color: "#000000",
51
+ },
52
+ ]);
53
+ mocks.discoverAgents.mockResolvedValue([
54
+ {
55
+ id: "clips",
56
+ name: "Clips",
57
+ description: "Record and share",
58
+ url: "https://clips.agent-native.com/share/WrA8ZQ3oxa2T?ref=clip_share",
59
+ color: "#000000",
60
+ },
61
+ ]);
62
+
63
+ const { default: action } = await import("./list-connected-agents.js");
64
+ const [clips] = await action.run({});
65
+
66
+ expect(clips).toMatchObject({
67
+ id: "clips",
68
+ url: "https://clips.agent-native.com/share/WrA8ZQ3oxa2T?ref=clip_share",
69
+ homeUrl: "https://clips.agent-native.com",
70
+ source: "builtin",
71
+ });
72
+ });
73
+ });
@@ -27,8 +27,10 @@ export default defineAction({
27
27
  (m) => m.getDispatchConfig(),
28
28
  );
29
29
  const discovered = await discoverAgents("dispatch");
30
- const builtinIds = new Set(
31
- getBuiltinAgents("dispatch").map((agent) => agent.id),
30
+ const builtins = getBuiltinAgents("dispatch");
31
+ const builtinIds = new Set(builtins.map((agent) => agent.id));
32
+ const builtinHomeUrls = new Map(
33
+ builtins.map((agent) => [agent.id, agent.url]),
32
34
  );
33
35
  const ownerEmail = getRequestUserEmail();
34
36
  if (!ownerEmail) throw new Error("no authenticated user");
@@ -78,8 +80,10 @@ export default defineAction({
78
80
  const connected = discovered.map((agent) => {
79
81
  const custom = customById.get(agent.id);
80
82
  const isBuiltin = builtinIds.has(agent.id);
83
+ const homeUrl = isBuiltin ? builtinHomeUrls.get(agent.id) : undefined;
81
84
  return {
82
85
  ...agent,
86
+ ...(homeUrl ? { homeUrl } : {}),
83
87
  source: isBuiltin ? "builtin" : custom ? "custom" : "workspace",
84
88
  resourceId: custom?.resourceId,
85
89
  path: custom?.path,
@@ -5,7 +5,7 @@ import { listDispatchUsageMetrics } from "../server/lib/usage-metrics-store.js";
5
5
 
6
6
  export default defineAction({
7
7
  description:
8
- "Get workspace-level LLM usage, spend or Builder.io credit spend, users, app access, and recent activity metrics for Dispatch admins.",
8
+ "Get personal or workspace LLM usage, spend or Builder.io credit spend, app attribution, prompt previews, and recent activity metrics for Dispatch.",
9
9
  schema: z.object({
10
10
  sinceDays: z.coerce
11
11
  .number()
@@ -14,6 +14,16 @@ export default defineAction({
14
14
  .max(365)
15
15
  .default(30)
16
16
  .describe("Lookback window in days. Defaults to 30."),
17
+ scope: z
18
+ .enum(["me", "workspace"])
19
+ .default("workspace")
20
+ .describe("Whether to inspect your account or the whole workspace."),
21
+ userEmail: z
22
+ .string()
23
+ .trim()
24
+ .min(1)
25
+ .optional()
26
+ .describe("Optional workspace member email to filter all usage to."),
17
27
  }),
18
28
  http: { method: "GET" },
19
29
  run: async (args) => listDispatchUsageMetrics(args),
@@ -68,6 +68,35 @@ describe("remix-workspace-template action", () => {
68
68
  );
69
69
  });
70
70
 
71
+ it("normalizes a human-friendly app name before scaffolding", async () => {
72
+ vi.stubEnv("NODE_ENV", "test");
73
+ vi.stubEnv("NETLIFY", "");
74
+ mocks.scaffoldWorkspaceAppFromTemplate.mockResolvedValue({
75
+ appId: "customer-portal",
76
+ template: "mail",
77
+ output: "scaffolded",
78
+ });
79
+
80
+ await action.run({
81
+ templateId: "mail",
82
+ appId: "Customer Portal",
83
+ });
84
+
85
+ expect(mocks.scaffoldWorkspaceAppFromTemplate).toHaveBeenCalledWith({
86
+ template: "mail",
87
+ appId: "customer-portal",
88
+ });
89
+ });
90
+
91
+ it("rejects names that normalize to an empty app id", async () => {
92
+ await expect(
93
+ action.run({ templateId: "mail", appId: "---" }),
94
+ ).rejects.toThrow(
95
+ "Use a non-reserved app id with lowercase letters, numbers, and hyphens.",
96
+ );
97
+ expect(mocks.scaffoldWorkspaceAppFromTemplate).not.toHaveBeenCalled();
98
+ });
99
+
71
100
  it("starts a hosted private app without copying data or secrets", async () => {
72
101
  vi.stubEnv("NODE_ENV", "production");
73
102
  mocks.startWorkspaceAppCreation.mockResolvedValue({
@@ -1,5 +1,8 @@
1
1
  import { defineAction } from "@agent-native/core";
2
- import { getWorkspaceAppIdValidationError } from "@agent-native/core/shared";
2
+ import {
3
+ getWorkspaceAppIdValidationError,
4
+ normalizeWorkspaceAppId,
5
+ } from "@agent-native/core/shared";
3
6
  import { z } from "zod";
4
7
 
5
8
  import {
@@ -42,14 +45,18 @@ export default defineAction({
42
45
  appId: z
43
46
  .string()
44
47
  .max(64)
48
+ .transform(normalizeWorkspaceAppId)
45
49
  .optional()
46
50
  .nullable()
47
- .refine((appId) => !appId || !getWorkspaceAppIdValidationError(appId), {
48
- message:
49
- "Use a non-reserved app id with lowercase letters, numbers, and hyphens.",
50
- })
51
+ .refine(
52
+ (appId) => appId == null || !getWorkspaceAppIdValidationError(appId),
53
+ {
54
+ message:
55
+ "Use a non-reserved app id with lowercase letters, numbers, and hyphens.",
56
+ },
57
+ )
51
58
  .describe(
52
- "Optional target workspace app id. Defaults to <template>-remix so the source app remains independent.",
59
+ "Optional target workspace app id or human-friendly name. Names are converted to lowercase, hyphenated ids. Defaults to <template>-remix so the source app remains independent.",
53
60
  ),
54
61
  description: z
55
62
  .string()
@@ -1,5 +1,8 @@
1
1
  import { defineAction } from "@agent-native/core";
2
- import { getWorkspaceAppIdValidationError } from "@agent-native/core/shared";
2
+ import {
3
+ getWorkspaceAppIdValidationError,
4
+ normalizeWorkspaceAppId,
5
+ } from "@agent-native/core/shared";
3
6
  import { z } from "zod";
4
7
 
5
8
  import { scaffoldWorkspaceAppFromTemplate } from "../server/lib/app-creation-store.js";
@@ -27,12 +30,16 @@ export default defineAction({
27
30
  appId: z
28
31
  .string()
29
32
  .max(64)
33
+ .transform(normalizeWorkspaceAppId)
30
34
  .optional()
31
35
  .nullable()
32
- .refine((appId) => !appId || !getWorkspaceAppIdValidationError(appId), {
33
- message:
34
- "Use a non-reserved app id with lowercase letters, numbers, and hyphens.",
35
- })
36
+ .refine(
37
+ (appId) => appId == null || !getWorkspaceAppIdValidationError(appId),
38
+ {
39
+ message:
40
+ "Use a non-reserved app id with lowercase letters, numbers, and hyphens.",
41
+ },
42
+ )
36
43
  .describe(
37
44
  "Optional override for the apps/<id>/ directory name; defaults to the template name",
38
45
  ),
@@ -1,5 +1,8 @@
1
1
  import { defineAction } from "@agent-native/core";
2
- import { getWorkspaceAppIdValidationError } from "@agent-native/core/shared";
2
+ import {
3
+ getWorkspaceAppIdValidationError,
4
+ normalizeWorkspaceAppId,
5
+ } from "@agent-native/core/shared";
3
6
  import { z } from "zod";
4
7
 
5
8
  import { startWorkspaceAppCreation } from "../server/lib/app-creation-store.js";
@@ -12,13 +15,16 @@ export default defineAction({
12
15
  appId: z
13
16
  .string()
14
17
  .max(64)
18
+ .transform(normalizeWorkspaceAppId)
15
19
  .refine((appId) => !getWorkspaceAppIdValidationError(appId), {
16
20
  message:
17
21
  "Use a non-reserved app id with lowercase letters, numbers, and hyphens.",
18
22
  })
19
23
  .optional()
20
24
  .nullable()
21
- .describe("Desired workspace app id/path"),
25
+ .describe(
26
+ "Desired workspace app id/path or human-friendly name. Names are converted to lowercase, hyphenated ids.",
27
+ ),
22
28
  template: z
23
29
  .string()
24
30
  .optional()
@@ -155,9 +155,21 @@ export default defineAction({
155
155
  }
156
156
  if (navigation?.view === "metrics") {
157
157
  try {
158
- const metrics = await listDispatchUsageMetrics({ sinceDays: 30 });
158
+ const usageScope =
159
+ navigation.usageScope === "workspace" ? "workspace" : "me";
160
+ const usageUserEmail =
161
+ typeof navigation.usageUserEmail === "string"
162
+ ? navigation.usageUserEmail
163
+ : undefined;
164
+ const metrics = await listDispatchUsageMetrics({
165
+ sinceDays: 30,
166
+ scope: usageScope,
167
+ userEmail: usageUserEmail,
168
+ });
159
169
  screen.usageMetrics = {
160
170
  billing: metrics.billing,
171
+ viewScope: metrics.viewScope,
172
+ selectedUserEmail: metrics.selectedUserEmail,
161
173
  totals: metrics.totals,
162
174
  byApp: metrics.byApp.slice(0, 8),
163
175
  byUser: metrics.byUser.slice(0, 8),
@@ -16,11 +16,14 @@ import {
16
16
  IconSettingsAutomation,
17
17
  IconShield,
18
18
  IconShieldCheck,
19
+ IconSearch,
20
+ IconX,
19
21
  } from "@tabler/icons-react";
20
- import { type ReactNode } from "react";
22
+ import { useMemo, useState, type ReactNode } from "react";
21
23
  import { NavLink } from "react-router";
22
24
 
23
25
  import { cn } from "../lib/utils";
26
+ import { DISPATCH_DOCS, DocsLink } from "./docs-link";
24
27
  import { useDispatchExtensions, type DispatchNavItem } from "./layout/Layout";
25
28
 
26
29
  export interface AdminNavGroup {
@@ -211,22 +214,60 @@ export function AdminShell({
211
214
  }) {
212
215
  const t = useT();
213
216
  const groups = useAdminNavGroups();
217
+ const [query, setQuery] = useState("");
218
+ const normalizedQuery = query.trim().toLowerCase();
219
+ const visibleGroups = useMemo(
220
+ () =>
221
+ groups
222
+ .map((group) => ({
223
+ ...group,
224
+ items: group.items.filter((item) => {
225
+ if (!normalizedQuery) return true;
226
+ const label = itemLabel(t, item).toLowerCase();
227
+ return `${label} ${group.label.toLowerCase()}`.includes(
228
+ normalizedQuery,
229
+ );
230
+ }),
231
+ }))
232
+ .filter((group) => group.items.length > 0),
233
+ [groups, normalizedQuery, t],
234
+ );
214
235
 
215
236
  return (
216
237
  <div
217
238
  className="grid min-w-0 gap-6 lg:grid-cols-[13rem_minmax(0,1fr)]"
218
239
  data-dispatch-admin-shell
219
240
  >
220
- <aside className="min-w-0 border-b pb-5 lg:sticky lg:top-2 lg:self-start lg:border-b-0 lg:border-e lg:pe-6">
221
- <div className="px-2">
222
- <div className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
223
- {t("dispatch.nav.admin", { defaultValue: "Admin" })}
241
+ <aside className="min-w-0 border-b pb-5 lg:sticky lg:top-2 lg:self-start lg:border-b-0">
242
+ <div className="flex items-center gap-1 px-2">
243
+ <div className="relative min-w-0 flex-1">
244
+ <IconSearch className="pointer-events-none absolute start-2.5 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground" />
245
+ <input
246
+ type="search"
247
+ value={query}
248
+ onChange={(event) => setQuery(event.target.value)}
249
+ onKeyDown={(event) => {
250
+ if (event.key === "Escape") setQuery("");
251
+ }}
252
+ placeholder="Search admin"
253
+ aria-label="Search admin"
254
+ className="h-8 w-full rounded-md border border-border bg-background ps-8 pe-7 text-[13px] text-foreground outline-none transition-colors placeholder:text-muted-foreground focus:border-foreground/30 focus:ring-2 focus:ring-accent/40"
255
+ />
256
+ {query ? (
257
+ <button
258
+ type="button"
259
+ onClick={() => setQuery("")}
260
+ aria-label="Clear admin search"
261
+ className="absolute end-1.5 top-1/2 flex size-5 -translate-y-1/2 items-center justify-center rounded text-muted-foreground hover:bg-accent/60 hover:text-foreground"
262
+ >
263
+ <IconX className="size-3.5" />
264
+ </button>
265
+ ) : null}
224
266
  </div>
225
- <p className="mt-1 text-xs leading-5 text-muted-foreground">
226
- {t("dispatch.pages.adminDescription", {
227
- defaultValue: "Workspace controls and operations",
228
- })}
229
- </p>
267
+ <DocsLink
268
+ href={DISPATCH_DOCS.dispatch}
269
+ label="Open Dispatch admin documentation"
270
+ />
230
271
  </div>
231
272
 
232
273
  <nav
@@ -235,7 +276,7 @@ export function AdminShell({
235
276
  })}
236
277
  className="mt-5 space-y-5"
237
278
  >
238
- {groups.map((group) => (
279
+ {visibleGroups.map((group) => (
239
280
  <section key={group.id}>
240
281
  <h2 className="px-2 text-[11px] font-medium uppercase tracking-wide text-muted-foreground/70">
241
282
  {t(group.labelKey, { defaultValue: group.label })}
@@ -251,6 +292,11 @@ export function AdminShell({
251
292
  </ul>
252
293
  </section>
253
294
  ))}
295
+ {visibleGroups.length === 0 ? (
296
+ <p className="px-2 py-6 text-center text-xs text-muted-foreground">
297
+ No matching admin pages
298
+ </p>
299
+ ) : null}
254
300
  </nav>
255
301
  </aside>
256
302
 
@@ -0,0 +1,34 @@
1
+ // @vitest-environment happy-dom
2
+ import React, { act } from "react";
3
+ import { createRoot, type Root } from "react-dom/client";
4
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
5
+
6
+ import { AppIcon } from "./app-icon";
7
+
8
+ describe("AppIcon", () => {
9
+ let container: HTMLDivElement;
10
+ let root: Root;
11
+
12
+ beforeEach(() => {
13
+ container = document.createElement("div");
14
+ document.body.appendChild(container);
15
+ root = createRoot(container);
16
+ });
17
+
18
+ afterEach(() => {
19
+ act(() => root.unmount());
20
+ container.remove();
21
+ });
22
+
23
+ it("gives Plan a colored generated accent instead of a neutral gray", () => {
24
+ act(() => {
25
+ root.render(<AppIcon id="plan" name="Plan" />);
26
+ });
27
+
28
+ const icon = container.firstElementChild as HTMLElement;
29
+ expect(icon.style.getPropertyValue("--dispatch-app-icon-color-rgb")).toBe(
30
+ "47 111 237",
31
+ );
32
+ expect(icon.style.color).toBe("rgb(var(--dispatch-app-icon-color-rgb))");
33
+ });
34
+ });
@@ -67,7 +67,7 @@ const APP_VISUALS_BY_ID: Record<
67
67
  forms: { icon: IconStack2, colorRgb: "6 182 212" },
68
68
  macros: { icon: IconCode, colorRgb: "113 113 122" },
69
69
  mail: { icon: IconMail, colorRgb: "59 130 246" },
70
- plan: { icon: IconLayoutBoard, colorRgb: "82 82 91" },
70
+ plan: { icon: IconLayoutBoard, colorRgb: "47 111 237" },
71
71
  slides: { icon: IconPresentation, colorRgb: "236 72 153" },
72
72
  tasks: { icon: IconListCheck, colorRgb: "99 102 241" },
73
73
  };
@@ -2,8 +2,7 @@ import type { ReactNode } from "react";
2
2
 
3
3
  import { cn } from "../lib/utils";
4
4
 
5
- export const APP_LIST_GRID_CLASS =
6
- "mx-auto w-full max-w-[1000px] xl:grid xl:grid-cols-2 xl:gap-3 xl:overflow-visible xl:rounded-none xl:bg-transparent";
5
+ export const APP_LIST_GRID_CLASS = "w-full xl:grid xl:grid-cols-2 xl:gap-3";
7
6
  export const APP_LIST_GRID_ROW_CLASS =
8
7
  "xl:rounded-2xl xl:border xl:border-transparent xl:bg-card xl:transition-[background-color,border-color] xl:hover:border-border xl:focus-within:border-ring/50";
9
8