@agent-native/dispatch 0.6.0 → 0.7.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 (159) hide show
  1. package/README.md +1 -1
  2. package/dist/actions/create-pylon-ticket.d.ts +3 -0
  3. package/dist/actions/create-pylon-ticket.d.ts.map +1 -0
  4. package/dist/actions/create-pylon-ticket.js +94 -0
  5. package/dist/actions/create-pylon-ticket.js.map +1 -0
  6. package/dist/actions/create-vault-grant.js +1 -1
  7. package/dist/actions/create-vault-grant.js.map +1 -1
  8. package/dist/actions/create-vault-secret.d.ts.map +1 -1
  9. package/dist/actions/create-vault-secret.js +4 -3
  10. package/dist/actions/create-vault-secret.js.map +1 -1
  11. package/dist/actions/get-vault-access-settings.d.ts +3 -0
  12. package/dist/actions/get-vault-access-settings.d.ts.map +1 -0
  13. package/dist/actions/get-vault-access-settings.js +10 -0
  14. package/dist/actions/get-vault-access-settings.js.map +1 -0
  15. package/dist/actions/grant-vault-secrets-to-app.js +1 -1
  16. package/dist/actions/grant-vault-secrets-to-app.js.map +1 -1
  17. package/dist/actions/index.d.ts.map +1 -1
  18. package/dist/actions/index.js +8 -0
  19. package/dist/actions/index.js.map +1 -1
  20. package/dist/actions/list-integrations-catalog.js +1 -1
  21. package/dist/actions/list-integrations-catalog.js.map +1 -1
  22. package/dist/actions/list-vault-grants.js +1 -1
  23. package/dist/actions/list-vault-grants.js.map +1 -1
  24. package/dist/actions/list-workspace-apps.d.ts.map +1 -1
  25. package/dist/actions/list-workspace-apps.js +5 -1
  26. package/dist/actions/list-workspace-apps.js.map +1 -1
  27. package/dist/actions/set-vault-access-settings.d.ts +3 -0
  28. package/dist/actions/set-vault-access-settings.d.ts.map +1 -0
  29. package/dist/actions/set-vault-access-settings.js +13 -0
  30. package/dist/actions/set-vault-access-settings.js.map +1 -0
  31. package/dist/actions/start-workspace-app-creation.d.ts.map +1 -1
  32. package/dist/actions/start-workspace-app-creation.js +6 -0
  33. package/dist/actions/start-workspace-app-creation.js.map +1 -1
  34. package/dist/actions/sync-vault-to-app.js +1 -1
  35. package/dist/actions/sync-vault-to-app.js.map +1 -1
  36. package/dist/actions/update-workspace-app-metadata.d.ts +3 -0
  37. package/dist/actions/update-workspace-app-metadata.d.ts.map +1 -0
  38. package/dist/actions/update-workspace-app-metadata.js +30 -0
  39. package/dist/actions/update-workspace-app-metadata.js.map +1 -0
  40. package/dist/actions/view-screen.d.ts.map +1 -1
  41. package/dist/actions/view-screen.js +4 -2
  42. package/dist/actions/view-screen.js.map +1 -1
  43. package/dist/components/app-keys-popover.d.ts.map +1 -1
  44. package/dist/components/app-keys-popover.js +17 -5
  45. package/dist/components/app-keys-popover.js.map +1 -1
  46. package/dist/components/create-app-popover.d.ts.map +1 -1
  47. package/dist/components/create-app-popover.js +38 -14
  48. package/dist/components/create-app-popover.js.map +1 -1
  49. package/dist/components/dispatch-shell.d.ts +4 -4
  50. package/dist/components/dispatch-shell.d.ts.map +1 -1
  51. package/dist/components/dispatch-shell.js +6 -6
  52. package/dist/components/dispatch-shell.js.map +1 -1
  53. package/dist/components/layout/Layout.d.ts.map +1 -1
  54. package/dist/components/layout/Layout.js +10 -3
  55. package/dist/components/layout/Layout.js.map +1 -1
  56. package/dist/components/messaging-setup-panel.d.ts.map +1 -1
  57. package/dist/components/messaging-setup-panel.js +2 -2
  58. package/dist/components/messaging-setup-panel.js.map +1 -1
  59. package/dist/components/workspace-app-card.d.ts.map +1 -1
  60. package/dist/components/workspace-app-card.js +41 -2
  61. package/dist/components/workspace-app-card.js.map +1 -1
  62. package/dist/hooks/use-navigation-state.js +12 -5
  63. package/dist/hooks/use-navigation-state.js.map +1 -1
  64. package/dist/lib/catch-all-target.d.ts +2 -0
  65. package/dist/lib/catch-all-target.d.ts.map +1 -0
  66. package/dist/lib/catch-all-target.js +95 -0
  67. package/dist/lib/catch-all-target.js.map +1 -0
  68. package/dist/lib/workspace-apps.d.ts +9 -0
  69. package/dist/lib/workspace-apps.d.ts.map +1 -1
  70. package/dist/lib/workspace-apps.js.map +1 -1
  71. package/dist/routes/pages/$appId.d.ts +2 -24
  72. package/dist/routes/pages/$appId.d.ts.map +1 -1
  73. package/dist/routes/pages/$appId.js +42 -8
  74. package/dist/routes/pages/$appId.js.map +1 -1
  75. package/dist/routes/pages/approval.d.ts.map +1 -1
  76. package/dist/routes/pages/approval.js +2 -1
  77. package/dist/routes/pages/approval.js.map +1 -1
  78. package/dist/routes/pages/apps.$appId.d.ts.map +1 -1
  79. package/dist/routes/pages/apps.$appId.js +2 -1
  80. package/dist/routes/pages/apps.$appId.js.map +1 -1
  81. package/dist/routes/pages/integrations.d.ts.map +1 -1
  82. package/dist/routes/pages/integrations.js +20 -15
  83. package/dist/routes/pages/integrations.js.map +1 -1
  84. package/dist/routes/pages/new-app.js +1 -1
  85. package/dist/routes/pages/new-app.js.map +1 -1
  86. package/dist/routes/pages/overview.d.ts.map +1 -1
  87. package/dist/routes/pages/overview.js +14 -1
  88. package/dist/routes/pages/overview.js.map +1 -1
  89. package/dist/routes/pages/vault.d.ts.map +1 -1
  90. package/dist/routes/pages/vault.js +25 -6
  91. package/dist/routes/pages/vault.js.map +1 -1
  92. package/dist/routes/pages/workspace.d.ts.map +1 -1
  93. package/dist/routes/pages/workspace.js +5 -3
  94. package/dist/routes/pages/workspace.js.map +1 -1
  95. package/dist/server/lib/app-creation-store.d.ts +13 -0
  96. package/dist/server/lib/app-creation-store.d.ts.map +1 -1
  97. package/dist/server/lib/app-creation-store.js +295 -9
  98. package/dist/server/lib/app-creation-store.js.map +1 -1
  99. package/dist/server/lib/env-config.d.ts.map +1 -1
  100. package/dist/server/lib/env-config.js +5 -0
  101. package/dist/server/lib/env-config.js.map +1 -1
  102. package/dist/server/lib/onboarding-steps.d.ts +12 -0
  103. package/dist/server/lib/onboarding-steps.d.ts.map +1 -0
  104. package/dist/server/lib/onboarding-steps.js +47 -0
  105. package/dist/server/lib/onboarding-steps.js.map +1 -0
  106. package/dist/server/lib/vault-store.d.ts +55 -0
  107. package/dist/server/lib/vault-store.d.ts.map +1 -1
  108. package/dist/server/lib/vault-store.js +210 -41
  109. package/dist/server/lib/vault-store.js.map +1 -1
  110. package/dist/server/plugins/agent-chat.d.ts.map +1 -1
  111. package/dist/server/plugins/agent-chat.js +2 -1
  112. package/dist/server/plugins/agent-chat.js.map +1 -1
  113. package/dist/server/plugins/core-routes.d.ts.map +1 -1
  114. package/dist/server/plugins/core-routes.js +4 -0
  115. package/dist/server/plugins/core-routes.js.map +1 -1
  116. package/dist/server/plugins/integrations.js +2 -2
  117. package/dist/server/plugins/integrations.js.map +1 -1
  118. package/package.json +13 -11
  119. package/src/actions/create-pylon-ticket.ts +109 -0
  120. package/src/actions/create-vault-grant.ts +1 -1
  121. package/src/actions/create-vault-secret.ts +4 -3
  122. package/src/actions/get-vault-access-settings.ts +11 -0
  123. package/src/actions/grant-vault-secrets-to-app.ts +1 -1
  124. package/src/actions/index.ts +8 -0
  125. package/src/actions/list-integrations-catalog.ts +1 -1
  126. package/src/actions/list-vault-grants.ts +1 -1
  127. package/src/actions/list-workspace-apps.ts +5 -1
  128. package/src/actions/set-vault-access-settings.ts +16 -0
  129. package/src/actions/start-workspace-app-creation.ts +8 -0
  130. package/src/actions/sync-vault-to-app.ts +1 -1
  131. package/src/actions/update-workspace-app-metadata.ts +32 -0
  132. package/src/actions/view-screen.ts +4 -1
  133. package/src/components/app-keys-popover.tsx +38 -8
  134. package/src/components/create-app-popover.tsx +47 -14
  135. package/src/components/dispatch-shell.tsx +16 -15
  136. package/src/components/layout/Layout.tsx +11 -5
  137. package/src/components/messaging-setup-panel.tsx +54 -39
  138. package/src/components/workspace-app-card.tsx +102 -0
  139. package/src/hooks/use-navigation-state.ts +10 -4
  140. package/src/lib/catch-all-target.spec.ts +218 -0
  141. package/src/lib/catch-all-target.ts +99 -0
  142. package/src/lib/workspace-apps.ts +9 -0
  143. package/src/routes/pages/$appId.tsx +45 -7
  144. package/src/routes/pages/approval.tsx +33 -3
  145. package/src/routes/pages/apps.$appId.tsx +6 -1
  146. package/src/routes/pages/integrations.tsx +57 -18
  147. package/src/routes/pages/new-app.tsx +1 -1
  148. package/src/routes/pages/overview.tsx +69 -29
  149. package/src/routes/pages/vault.tsx +101 -21
  150. package/src/routes/pages/workspace.tsx +21 -3
  151. package/src/server/lib/app-creation-store.spec.ts +61 -2
  152. package/src/server/lib/app-creation-store.ts +386 -11
  153. package/src/server/lib/env-config.ts +5 -0
  154. package/src/server/lib/onboarding-steps.ts +49 -0
  155. package/src/server/lib/vault-store.spec.ts +69 -0
  156. package/src/server/lib/vault-store.ts +266 -49
  157. package/src/server/plugins/agent-chat.ts +2 -1
  158. package/src/server/plugins/core-routes.ts +5 -0
  159. package/src/server/plugins/integrations.ts +2 -2
@@ -3,6 +3,7 @@ import approveDispatchChange from "./approve-dispatch-change.js";
3
3
  import approveVaultRequest from "./approve-vault-request.js";
4
4
  import archiveWorkspaceApp from "./archive-workspace-app.js";
5
5
  import createLinkToken from "./create-link-token.js";
6
+ import createPylonTicket from "./create-pylon-ticket.js";
6
7
  import createVaultGrant from "./create-vault-grant.js";
7
8
  import createVaultSecret from "./create-vault-secret.js";
8
9
  import createWorkspaceResourceGrant from "./create-workspace-resource-grant.js";
@@ -14,6 +15,7 @@ import denyVaultRequest from "./deny-vault-request.js";
14
15
  import getAppCreationSettings from "./get-app-creation-settings.js";
15
16
  import getAgentThreadDebug from "./get-agent-thread-debug.js";
16
17
  import getDispatchSettings from "./get-dispatch-settings.js";
18
+ import getVaultAccessSettings from "./get-vault-access-settings.js";
17
19
  import getWorkspaceInfo from "./get-workspace-info.js";
18
20
  import grantWorkspaceResourcesToApp from "./grant-workspace-resources-to-app.js";
19
21
  import grantVaultSecretsToApp from "./grant-vault-secrets-to-app.js";
@@ -47,11 +49,13 @@ import searchAgentThreads from "./search-agent-threads.js";
47
49
  import sendPlatformMessage from "./send-platform-message.js";
48
50
  import setAppCreationSettings from "./set-app-creation-settings.js";
49
51
  import setDispatchApprovalPolicy from "./set-dispatch-approval-policy.js";
52
+ import setVaultAccessSettings from "./set-vault-access-settings.js";
50
53
  import startWorkspaceAppCreation from "./start-workspace-app-creation.js";
51
54
  import syncVaultToApp from "./sync-vault-to-app.js";
52
55
  import syncWorkspaceResourcesToAll from "./sync-workspace-resources-to-all.js";
53
56
  import syncWorkspaceResourcesToApp from "./sync-workspace-resources-to-app.js";
54
57
  import unarchiveWorkspaceApp from "./unarchive-workspace-app.js";
58
+ import updateWorkspaceAppMetadata from "./update-workspace-app-metadata.js";
55
59
  import updateVaultSecret from "./update-vault-secret.js";
56
60
  import updateWorkspaceResource from "./update-workspace-resource.js";
57
61
  import upsertDestination from "./upsert-destination.js";
@@ -68,6 +72,7 @@ export const dispatchActions: Record<string, ActionEntry> = {
68
72
  "approve-vault-request": approveVaultRequest,
69
73
  "archive-workspace-app": archiveWorkspaceApp,
70
74
  "create-link-token": createLinkToken,
75
+ "create-pylon-ticket": createPylonTicket,
71
76
  "create-vault-grant": createVaultGrant,
72
77
  "create-vault-secret": createVaultSecret,
73
78
  "create-workspace-resource-grant": createWorkspaceResourceGrant,
@@ -79,6 +84,7 @@ export const dispatchActions: Record<string, ActionEntry> = {
79
84
  "get-app-creation-settings": getAppCreationSettings,
80
85
  "get-agent-thread-debug": getAgentThreadDebug,
81
86
  "get-dispatch-settings": getDispatchSettings,
87
+ "get-vault-access-settings": getVaultAccessSettings,
82
88
  "get-workspace-info": getWorkspaceInfo,
83
89
  "grant-workspace-resources-to-app": grantWorkspaceResourcesToApp,
84
90
  "grant-vault-secrets-to-app": grantVaultSecretsToApp,
@@ -112,11 +118,13 @@ export const dispatchActions: Record<string, ActionEntry> = {
112
118
  "send-platform-message": sendPlatformMessage,
113
119
  "set-app-creation-settings": setAppCreationSettings,
114
120
  "set-dispatch-approval-policy": setDispatchApprovalPolicy,
121
+ "set-vault-access-settings": setVaultAccessSettings,
115
122
  "start-workspace-app-creation": startWorkspaceAppCreation,
116
123
  "sync-vault-to-app": syncVaultToApp,
117
124
  "sync-workspace-resources-to-all": syncWorkspaceResourcesToAll,
118
125
  "sync-workspace-resources-to-app": syncWorkspaceResourcesToApp,
119
126
  "unarchive-workspace-app": unarchiveWorkspaceApp,
127
+ "update-workspace-app-metadata": updateWorkspaceAppMetadata,
120
128
  "update-vault-secret": updateVaultSecret,
121
129
  "update-workspace-resource": updateWorkspaceResource,
122
130
  "upsert-destination": upsertDestination,
@@ -4,7 +4,7 @@ import { listIntegrationsCatalog } from "../server/lib/vault-store.js";
4
4
 
5
5
  export default defineAction({
6
6
  description:
7
- "List all workspace apps and their credential/integration requirements. Shows which credentials are configured, which are granted from the vault, and which are missing.",
7
+ "List all workspace apps and their credential/integration requirements. Shows configured credentials and effective Dispatch vault access.",
8
8
  schema: z.object({}),
9
9
  http: { method: "GET" },
10
10
  run: async () => listIntegrationsCatalog(),
@@ -4,7 +4,7 @@ import { listGrants } from "../server/lib/vault-store.js";
4
4
 
5
5
  export default defineAction({
6
6
  description:
7
- "List vault grants which apps have access to which secrets. Optionally filter by app or secret.",
7
+ "List explicit vault grants used by manual vault access mode. Optionally filter by app or secret.",
8
8
  schema: z.object({
9
9
  appId: z.string().optional().describe("Filter by app ID"),
10
10
  secretId: z.string().optional().describe("Filter by secret ID"),
@@ -12,13 +12,17 @@ const httpBoolean = z.preprocess((value) => {
12
12
 
13
13
  export default defineAction({
14
14
  description:
15
- "List apps installed in this workspace, including mounted paths, absolute URLs, and agent-card/A2A metadata for ready apps by default. UI polling callers can pass includeAgentCards=false to skip network probes.",
15
+ "List apps installed in this workspace, including mounted paths, absolute URLs, audience (internal/public), page route access overrides, and agent-card/A2A metadata for ready apps by default. UI polling callers can pass includeAgentCards=false to skip network probes.",
16
16
  schema: z.object({
17
17
  includeAgentCards: httpBoolean
18
18
  .default(true)
19
19
  .describe(
20
20
  "Fetch each ready app's /.well-known/agent-card.json with a short non-throwing timeout and include agentCardUrl, agentCardReachable, a2aEndpointUrl, agentName, and agentSkillsCount. Defaults to true for agent calls; UI polling should pass false. Pending Builder apps are not probed.",
21
21
  ),
22
+ audience: z
23
+ .enum(["all", "internal", "public"])
24
+ .default("all")
25
+ .describe("Filter by workspace app audience."),
22
26
  }),
23
27
  http: { method: "GET" },
24
28
  run: async (input) => listWorkspaceApps(input),
@@ -0,0 +1,16 @@
1
+ import { defineAction } from "@agent-native/core";
2
+ import { z } from "zod";
3
+ import { setVaultAccessSettings } from "../server/lib/vault-store.js";
4
+
5
+ export default defineAction({
6
+ description:
7
+ "Set the Dispatch vault access mode. Use all-apps for the default workspace-wide mode or manual to require explicit per-app grants.",
8
+ schema: z.object({
9
+ mode: z
10
+ .enum(["all-apps", "manual"])
11
+ .describe(
12
+ "all-apps shares every vault key with every app; manual requires grants",
13
+ ),
14
+ }),
15
+ run: async (args) => setVaultAccessSettings(args),
16
+ });
@@ -23,6 +23,14 @@ export default defineAction({
23
23
  .optional()
24
24
  .nullable()
25
25
  .describe("Template to start from"),
26
+ description: z
27
+ .string()
28
+ .max(500)
29
+ .optional()
30
+ .nullable()
31
+ .describe(
32
+ "Concise AI-generated description of the app based on the user's prompt. Dispatch saves this while the app is being created.",
33
+ ),
26
34
  secretIds: z
27
35
  .array(z.string())
28
36
  .max(100)
@@ -4,7 +4,7 @@ import { syncGrantsToApp } from "../server/lib/vault-store.js";
4
4
 
5
5
  export default defineAction({
6
6
  description:
7
- "Push all granted secrets to an app by calling its env-vars endpoint. Returns the list of synced credential keys.",
7
+ "Push vault secrets to an app by calling its env-vars endpoint. In all-apps mode this syncs every vault key; in manual mode it syncs active grants.",
8
8
  schema: z.object({
9
9
  appId: z
10
10
  .string()
@@ -0,0 +1,32 @@
1
+ import { defineAction } from "@agent-native/core";
2
+ import { getWorkspaceAppIdValidationError } from "@agent-native/core/shared";
3
+ import { z } from "zod";
4
+ import { updateWorkspaceAppMetadata } from "../server/lib/app-creation-store.js";
5
+
6
+ export default defineAction({
7
+ description:
8
+ "Update the human-editable display name and description Dispatch uses for a workspace app. These details are also used as connected-agent/A2A context.",
9
+ schema: z.object({
10
+ appId: z
11
+ .string()
12
+ .max(64)
13
+ .refine((appId) => !getWorkspaceAppIdValidationError(appId), {
14
+ message:
15
+ "Use a non-reserved app id with lowercase letters, numbers, and hyphens.",
16
+ })
17
+ .describe("Workspace app id, matching the apps/<id> folder"),
18
+ name: z
19
+ .string()
20
+ .max(120)
21
+ .optional()
22
+ .nullable()
23
+ .describe("Human-readable app name"),
24
+ description: z
25
+ .string()
26
+ .max(500)
27
+ .optional()
28
+ .nullable()
29
+ .describe("Human-readable app description"),
30
+ }),
31
+ run: async (args) => updateWorkspaceAppMetadata(args),
32
+ });
@@ -16,6 +16,7 @@ import {
16
16
  listSecrets,
17
17
  listGrants,
18
18
  listRequests,
19
+ getVaultAccessSettings,
19
20
  } from "../server/lib/vault-store.js";
20
21
  import { listWorkspaceApps } from "../server/lib/app-creation-store.js";
21
22
  import { listDispatchUsageMetrics } from "../server/lib/usage-metrics-store.js";
@@ -78,11 +79,13 @@ export default defineAction({
78
79
  }
79
80
  }
80
81
  if (navigation?.view === "vault" || navigation?.view === "new-app") {
81
- const [secrets, grants, requests] = await Promise.all([
82
+ const [secrets, grants, requests, access] = await Promise.all([
82
83
  listSecrets(),
83
84
  listGrants(),
84
85
  listRequests({ status: "pending" }),
86
+ getVaultAccessSettings(),
85
87
  ]);
88
+ screen.vaultAccessMode = access.mode;
86
89
  screen.vaultSecrets = secrets.map((s) => ({
87
90
  id: s.id,
88
91
  name: s.name,
@@ -13,6 +13,7 @@ import {
13
13
  PopoverTrigger,
14
14
  } from "@/components/ui/popover";
15
15
  import { Button } from "@/components/ui/button";
16
+ import { Skeleton } from "@/components/ui/skeleton";
16
17
 
17
18
  interface VaultSecret {
18
19
  id: string;
@@ -88,6 +89,12 @@ function AppKeysPanel({ appId, appName }: { appId: string; appName: string }) {
88
89
  isLoading: grantsLoading,
89
90
  refetch: refetchGrants,
90
91
  } = useActionQuery("list-vault-grants", { appId });
92
+ const { data: accessSettings, isLoading: accessLoading } = useActionQuery(
93
+ "get-vault-access-settings",
94
+ {},
95
+ );
96
+ const accessMode =
97
+ (accessSettings as any)?.mode === "manual" ? "manual" : "all-apps";
91
98
 
92
99
  const grantBySecretId = useMemo(() => {
93
100
  const map = new Map<string, VaultGrant>();
@@ -134,11 +141,13 @@ function AppKeysPanel({ appId, appName }: { appId: string; appName: string }) {
134
141
  onError: (err) => toast.error(`Sync failed: ${String(err)}`),
135
142
  });
136
143
 
137
- const isLoading = secretsLoading || grantsLoading;
144
+ const isLoading = secretsLoading || grantsLoading || accessLoading;
138
145
  const grantedCount = grantBySecretId.size;
139
146
  const typedSecrets = secrets as VaultSecret[];
147
+ const allApps = accessMode !== "manual";
140
148
 
141
149
  const toggleSecret = (secret: VaultSecret) => {
150
+ if (allApps) return;
142
151
  if (pendingSecretIds.has(secret.id)) return;
143
152
  const existing = grantBySecretId.get(secret.id);
144
153
  markPending(secret.id, true);
@@ -158,14 +167,20 @@ function AppKeysPanel({ appId, appName }: { appId: string; appName: string }) {
158
167
  Keys for {appName}
159
168
  </p>
160
169
  <p className="text-[11px] text-muted-foreground">
161
- {grantedCount} of {typedSecrets.length} granted
170
+ {allApps
171
+ ? `${typedSecrets.length} available`
172
+ : `${grantedCount} of ${typedSecrets.length} granted`}
162
173
  </p>
163
174
  </div>
164
175
  <Button
165
176
  type="button"
166
177
  variant="outline"
167
178
  size="sm"
168
- disabled={syncMutation.isPending || grantedCount === 0}
179
+ disabled={
180
+ syncMutation.isPending ||
181
+ typedSecrets.length === 0 ||
182
+ (!allApps && grantedCount === 0)
183
+ }
169
184
  onClick={() => syncMutation.mutate({ appId })}
170
185
  className="h-7 px-2"
171
186
  >
@@ -180,24 +195,37 @@ function AppKeysPanel({ appId, appName }: { appId: string; appName: string }) {
180
195
 
181
196
  <div className="max-h-[320px] space-y-1.5 overflow-y-auto rounded-md border border-border bg-card p-1.5">
182
197
  {isLoading ? (
183
- <p className="px-3 py-3 text-xs text-muted-foreground">Loading…</p>
198
+ <div className="space-y-1.5 p-1.5">
199
+ {Array.from({ length: 3 }).map((_, index) => (
200
+ <div
201
+ key={index}
202
+ className="flex items-start gap-3 rounded-md px-2.5 py-2"
203
+ >
204
+ <Skeleton className="mt-0.5 h-4 w-4 shrink-0 rounded" />
205
+ <div className="flex-1 space-y-1.5">
206
+ <Skeleton className="h-3 w-1/2" />
207
+ <Skeleton className="h-3 w-3/4" />
208
+ </div>
209
+ </div>
210
+ ))}
211
+ </div>
184
212
  ) : typedSecrets.length === 0 ? (
185
213
  <p className="rounded-md border border-dashed border-border px-3 py-3 text-xs text-muted-foreground">
186
214
  No vault keys yet. Add one from the Vault page.
187
215
  </p>
188
216
  ) : (
189
217
  typedSecrets.map((secret) => {
190
- const granted = grantBySecretId.has(secret.id);
218
+ const granted = allApps || grantBySecretId.has(secret.id);
191
219
  const pending = pendingSecretIds.has(secret.id);
192
220
  return (
193
221
  <button
194
222
  key={secret.id}
195
223
  type="button"
196
224
  aria-pressed={granted}
197
- disabled={pending}
225
+ disabled={pending || allApps}
198
226
  onClick={() => toggleSecret(secret)}
199
227
  className={`flex w-full items-start gap-3 rounded-md px-2.5 py-2 text-left text-sm disabled:cursor-not-allowed disabled:opacity-60 ${
200
- pending ? "" : "cursor-pointer"
228
+ pending || allApps ? "" : "cursor-pointer"
201
229
  } ${
202
230
  granted
203
231
  ? "border border-primary/45 bg-primary/5"
@@ -218,7 +246,9 @@ function AppKeysPanel({ appId, appName }: { appId: string; appName: string }) {
218
246
  {secret.credentialKey}
219
247
  </span>
220
248
  <span className="block truncate text-xs text-muted-foreground/70">
221
- {secret.provider || secret.name || "Vault secret"}
249
+ {allApps
250
+ ? "Available to this app"
251
+ : secret.provider || secret.name || "Vault secret"}
222
252
  </span>
223
253
  </span>
224
254
  </button>
@@ -44,6 +44,8 @@ interface WorkspaceResourceOption {
44
44
  updatedAt?: number;
45
45
  }
46
46
 
47
+ type VaultAccessMode = "all-apps" | "manual";
48
+
47
49
  interface CreateAppPopoverProps {
48
50
  /**
49
51
  * Custom trigger element. Defaults to a dashed-border tile that matches the
@@ -78,11 +80,15 @@ function buildAppCreationPrompt(input: {
78
80
  prompt: string;
79
81
  selectedKeys: string[];
80
82
  selectedResources: WorkspaceResourceOption[];
83
+ vaultAccessMode: VaultAccessMode;
81
84
  }): string {
82
85
  const keyList = input.selectedKeys.join(", ");
83
- const grantRequest = keyList
84
- ? `Requested Dispatch vault key grants for this app: ${keyList}`
85
- : `Requested Dispatch vault key grants for this app: none`;
86
+ const grantRequest =
87
+ input.vaultAccessMode === "all-apps"
88
+ ? `Dispatch vault access: all saved vault keys are available to every workspace app by default. No per-app vault grants are needed.`
89
+ : keyList
90
+ ? `Requested Dispatch vault key grants for this app: ${keyList}`
91
+ : `Requested Dispatch vault key grants for this app: none`;
86
92
  const resourceList = input.selectedResources.length
87
93
  ? input.selectedResources
88
94
  .map(
@@ -98,6 +104,7 @@ function buildAppCreationPrompt(input: {
98
104
  ``,
99
105
  `Suggested app name: ${input.appId} (you may adjust the slug if it conflicts)`,
100
106
  `User prompt: ${input.prompt.trim()}`,
107
+ `Generate a concise one-sentence app description from the user prompt before coding; save it in apps/${input.appId}/package.json "description" so Dispatch and A2A can describe the app.`,
101
108
  `If the user mentions a product or company such as Granola, Loom, Superhuman, Linear, or Notion, treat it as product inspiration unless they explicitly ask to connect to that service. Do not invent or require third-party API keys like GRANOLA_API_KEY just because a product is named.`,
102
109
  grantRequest,
103
110
  `Requested Dispatch workspace resources for this app:\n${resourceList}`,
@@ -112,18 +119,20 @@ function buildAppCreationPrompt(input: {
112
119
  `Do not clone first-party templates, create wrapper apps, or scaffold child apps/routes for Mail, Calendar, Analytics, etc. inside apps/${input.appId} just so this app can access them. If the request is a cross-app dashboard or overview, build only the new dashboard/overview app and delegate to the existing apps for domain work.`,
113
120
  `Only create another first-party app copy when the user explicitly asks for a customized fork/copy of that app; otherwise keep using the hosted/shared app so improvements to the base template keep flowing to users.`,
114
121
  `Do not satisfy this by adding a route, page, component, or file inside apps/starter or another existing app unless the user explicitly asks to modify that existing app.`,
115
- keyList
116
- ? `After the app exists, grant the selected Dispatch vault keys to appId "${input.appId}" and sync them once the app server is available. Treat these as requested grants, not active grants before creation succeeds.`
117
- : `Do not grant any Dispatch vault keys unless the user asks later.`,
122
+ input.vaultAccessMode === "all-apps"
123
+ ? `Do not create per-app Dispatch vault grants unless the workspace switches vault access to manual or the user explicitly asks for manual grants.`
124
+ : keyList
125
+ ? `After the app exists, grant the selected Dispatch vault keys to appId "${input.appId}" and sync them once the app server is available. Treat these as requested grants, not active grants before creation succeeds.`
126
+ : `Do not grant any Dispatch vault keys unless the user asks later.`,
118
127
  input.selectedResources.length
119
128
  ? `After the app exists, grant the selected Dispatch workspace resources to appId "${input.appId}" and sync them once the app server is available. Add a short note to apps/${input.appId}/AGENTS.md telling the app agent to read relevant shared resources under context/ or the selected resource paths before doing GTM/domain work.`
120
129
  : `Do not grant any Dispatch workspace resources unless the user asks later.`,
121
130
  ``,
122
131
  `App readiness requirements before handing off:`,
123
- `- Ensure apps/${input.appId}/package.json exists; Dispatch discovers workspace apps from apps/<app-id>/package.json, not a separate app registry.`,
132
+ `- Ensure apps/${input.appId}/package.json exists with displayName/name and a concise description; Dispatch discovers workspace apps from apps/<app-id>/package.json, not a separate app registry.`,
124
133
  `- Update the app manifest/package/deploy metadata needed by the existing workspace deployment model.`,
125
134
  `- Ensure the React Router client entry preserves APP_BASE_PATH/VITE_APP_BASE_PATH via appBasePath() so /${input.appId} hydrates correctly.`,
126
- `- Verify the app's agent card/A2A metadata is ready so Dispatch can discover and delegate to the app after deployment.`,
135
+ `- Verify the app's agent card/A2A metadata is ready so Dispatch can discover and delegate to the app after deployment. Every sibling workspace app is available over A2A by default through call-agent, with names and descriptions from the workspace app registry.`,
127
136
  `When it is ready, start or update the workspace dev server and navigate the user to the absolute path /${input.appId} on the workspace origin. Do not prefix with /dispatch/, /apps/, /workspace/, or any other Dispatch tab — the new app is mounted at the workspace root, not under Dispatch. If you have a navigate tool available, pass /${input.appId} verbatim; if you only have a window.location-style escape hatch, set it to /${input.appId}.`,
128
137
  ].join("\n");
129
138
  }
@@ -170,6 +179,8 @@ export function CreateAppFlow({
170
179
  const [selectedResourceIds, setSelectedResourceIds] = useState<string[]>([]);
171
180
  const [secrets, setSecrets] = useState<VaultSecretOption[]>([]);
172
181
  const [resources, setResources] = useState<WorkspaceResourceOption[]>([]);
182
+ const [vaultAccessMode, setVaultAccessMode] =
183
+ useState<VaultAccessMode>("all-apps");
173
184
  const [secretsError, setSecretsError] = useState<string | null>(null);
174
185
  const [resourcesError, setResourcesError] = useState<string | null>(null);
175
186
  const [statusMessage, setStatusMessage] = useState<string | null>(null);
@@ -193,6 +204,15 @@ export function CreateAppFlow({
193
204
  setSecrets([]);
194
205
  setSecretsError(err?.message || "Could not load Dispatch keys");
195
206
  });
207
+ fetchJson(actionUrl(basePath, "get-vault-access-settings"))
208
+ .then((data) => {
209
+ if (cancelled) return;
210
+ setVaultAccessMode(data?.mode === "manual" ? "manual" : "all-apps");
211
+ })
212
+ .catch(() => {
213
+ if (cancelled) return;
214
+ setVaultAccessMode("manual");
215
+ });
196
216
  fetchJson(actionUrl(basePath, "list-workspace-resource-options"))
197
217
  .then((data) => {
198
218
  if (cancelled) return;
@@ -218,9 +238,11 @@ export function CreateAppFlow({
218
238
  [resources, selectedResourceIds],
219
239
  );
220
240
  const selectedSecretLabel =
221
- selectedSecretIds.length === 0
222
- ? "no keys"
223
- : `${selectedSecretIds.length} key${selectedSecretIds.length === 1 ? "" : "s"}`;
241
+ vaultAccessMode === "all-apps"
242
+ ? "all keys"
243
+ : selectedSecretIds.length === 0
244
+ ? "no keys"
245
+ : `${selectedSecretIds.length} key${selectedSecretIds.length === 1 ? "" : "s"}`;
224
246
  const selectedResourceLabel =
225
247
  selectedResourceIds.length === 0
226
248
  ? "no resources"
@@ -254,8 +276,12 @@ export function CreateAppFlow({
254
276
  const message = buildAppCreationPrompt({
255
277
  appId,
256
278
  prompt: trimmed,
257
- selectedKeys: selectedSecrets.map((s) => s.credentialKey),
279
+ selectedKeys:
280
+ vaultAccessMode === "manual"
281
+ ? selectedSecrets.map((s) => s.credentialKey)
282
+ : [],
258
283
  selectedResources,
284
+ vaultAccessMode,
259
285
  });
260
286
  setIsSubmitting(true);
261
287
  setStatusMessage(null);
@@ -279,7 +305,10 @@ export function CreateAppFlow({
279
305
  body: JSON.stringify({
280
306
  prompt: trimmed,
281
307
  appId,
282
- secretIds: selectedSecretIds.length > 0 ? selectedSecretIds : [],
308
+ secretIds:
309
+ vaultAccessMode === "manual" && selectedSecretIds.length > 0
310
+ ? selectedSecretIds
311
+ : [],
283
312
  resourceIds:
284
313
  selectedResourceIds.length > 0 ? selectedResourceIds : [],
285
314
  }),
@@ -351,7 +380,11 @@ export function CreateAppFlow({
351
380
  <IconKey size={12} />
352
381
  Dispatch keys
353
382
  </div>
354
- {secretsError ? (
383
+ {vaultAccessMode === "all-apps" ? (
384
+ <p className="rounded-md border border-dashed border-border px-3 py-3 text-xs text-muted-foreground">
385
+ Every saved Dispatch vault key is available to new apps.
386
+ </p>
387
+ ) : secretsError ? (
355
388
  <p className="rounded-md border border-dashed border-border px-3 py-3 text-xs text-muted-foreground">
356
389
  {secretsError}
357
390
  </p>
@@ -1,17 +1,17 @@
1
1
  import { type ReactNode } from "react";
2
2
  import {
3
- Tooltip,
4
- TooltipContent,
5
- TooltipTrigger,
6
- } from "@/components/ui/tooltip";
3
+ Popover,
4
+ PopoverContent,
5
+ PopoverTrigger,
6
+ } from "@/components/ui/popover";
7
7
  import { IconInfoCircle } from "@tabler/icons-react";
8
8
  import { useSetPageTitle } from "@/components/layout/HeaderActions";
9
9
 
10
10
  /**
11
- * DispatchShell renders the per-page title (with optional description tooltip)
12
- * into the global header via the HeaderActions store. The actual chrome
13
- * (sidebar, AgentSidebar, header bar with AgentToggleButton) is provided by
14
- * `Layout` mounted in `root.tsx`.
11
+ * DispatchShell renders the per-page title (with an optional click-to-open
12
+ * description popover) into the global header via the HeaderActions store.
13
+ * The actual chrome (sidebar, AgentSidebar, header bar with AgentToggleButton)
14
+ * is provided by `Layout` mounted in `root.tsx`.
15
15
  */
16
16
  export function DispatchShell({
17
17
  title,
@@ -28,23 +28,24 @@ export function DispatchShell({
28
28
  {title}
29
29
  </h1>
30
30
  {description ? (
31
- <Tooltip>
32
- <TooltipTrigger asChild>
31
+ <Popover>
32
+ <PopoverTrigger asChild>
33
33
  <button
34
34
  type="button"
35
- className="text-muted-foreground/60 hover:text-foreground cursor-pointer"
35
+ className="inline-flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground/70 hover:bg-accent hover:text-foreground cursor-pointer"
36
36
  aria-label={`About ${title}`}
37
37
  >
38
38
  <IconInfoCircle className="h-3.5 w-3.5" />
39
39
  </button>
40
- </TooltipTrigger>
41
- <TooltipContent
40
+ </PopoverTrigger>
41
+ <PopoverContent
42
42
  side="bottom"
43
+ align="start"
43
44
  className="max-w-72 text-xs leading-relaxed"
44
45
  >
45
46
  {description}
46
- </TooltipContent>
47
- </Tooltip>
47
+ </PopoverContent>
48
+ </Popover>
48
49
  ) : null}
49
50
  </div>,
50
51
  );
@@ -229,10 +229,16 @@ function dispatchNavLinkTarget(path: string): string {
229
229
  if (typeof window === "undefined") return path;
230
230
  const basePath = appBasePath();
231
231
  if (!basePath) return path;
232
- const context = (
233
- window as Window & { __reactRouterContext?: { basename?: string } }
234
- ).__reactRouterContext;
235
- return context?.basename === basePath ? path : appPath(path);
232
+ // Mirror the basename calculation entry.client.tsx uses to configure the
233
+ // router (basePath iff the current URL is under that mount, "" otherwise).
234
+ // Reading the live URL directly avoids races with the previous check on
235
+ // `__reactRouterContext.basename`, which could read undefined before the
236
+ // entry script set it — that race produced /dispatch/dispatch/<route>
237
+ // history entries that 404'd on back-button navigation.
238
+ const pathname = window.location.pathname;
239
+ const routerHasBasename =
240
+ pathname === basePath || pathname.startsWith(`${basePath}/`);
241
+ return routerHasBasename ? path : appPath(path);
236
242
  }
237
243
 
238
244
  export function NavContent({
@@ -424,7 +430,7 @@ export function Layout({
424
430
  <AgentSidebar
425
431
  position="right"
426
432
  defaultOpen={false}
427
- emptyStateText="Create apps, grant keys, and route work across the workspace."
433
+ emptyStateText="Create apps, manage vault keys, and route work across the workspace."
428
434
  suggestions={SIDEBAR_SUGGESTIONS}
429
435
  >
430
436
  {appContent}